diff options
163 files changed, 5334 insertions, 3137 deletions
@@ -56,7 +56,7 @@ GO_LINKER_FLAGS ?= -ldflags \ # GOOS/GOARCH of the build host, used to determine whether we're cross-compiling or not BUILDER_GOOS_GOARCH="$(shell $(GO) env GOOS)_$(shell $(GO) env GOARCH)" -PLATFORM_FILES=$(shell ls -1 ./cmd/platform/*.go | grep -v _test.go) +PLATFORM_FILES="./main.go" # Output paths DIST_ROOT=dist @@ -118,7 +118,7 @@ start-docker: ## Starts the docker containers for local development. @if [ $(shell docker ps -a | grep -ci mattermost-inbucket) -eq 0 ]; then \ echo starting mattermost-inbucket; \ - docker run --name mattermost-inbucket -p 9000:10080 -p 2500:10025 -d jhillyerd/inbucket:latest > /dev/null; \ + docker run --name mattermost-inbucket -p 9000:10080 -p 2500:10025 -d jhillyerd/inbucket:release-1.2.0 > /dev/null; \ elif [ $(shell docker ps | grep -ci mattermost-inbucket) -eq 0 ]; then \ echo restarting mattermost-inbucket; \ docker start mattermost-inbucket > /dev/null; \ diff --git a/api/admin.go b/api/admin.go index 3b58650cc..6016e48f3 100644 --- a/api/admin.go +++ b/api/admin.go @@ -7,10 +7,10 @@ import ( "net/http" "strconv" + "github.com/avct/uasurfer" "github.com/gorilla/mux" "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" - "github.com/mssola/user_agent" ) func (api *API) InitAdmin() { @@ -201,12 +201,11 @@ func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer // attach extra headers to trigger a download on IE, Edge, and Safari - ua := user_agent.New(r.UserAgent()) - bname, _ := ua.Browser() + ua := uasurfer.Parse(r.UserAgent()) w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"") - if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" { + if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari { // trim off anything before the final / so we just get the file's name w.Header().Set("Content-Type", "application/octet-stream") } diff --git a/api/channel_test.go b/api/channel_test.go index 9268d9071..6d9eb5538 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -11,7 +11,6 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" - "github.com/mattermost/mattermost-server/store/sqlstore" ) func TestCreateChannel(t *testing.T) { @@ -374,7 +373,7 @@ func TestUpdateChannel(t *testing.T) { }) th.MakeUserChannelUser(th.BasicUser, channel2) th.MakeUserChannelUser(th.BasicUser, channel3) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() if _, err := Client.UpdateChannel(channel2); err == nil { t.Fatal("should have errored not channel admin") @@ -396,7 +395,7 @@ func TestUpdateChannel(t *testing.T) { th.MakeUserChannelAdmin(th.BasicUser, channel2) th.MakeUserChannelAdmin(th.BasicUser, channel3) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() if _, err := Client.UpdateChannel(channel2); err != nil { t.Fatal(err) @@ -596,7 +595,7 @@ func TestUpdateChannelHeader(t *testing.T) { }) th.MakeUserChannelUser(th.BasicUser, channel2) th.MakeUserChannelUser(th.BasicUser, channel3) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() if _, err := Client.UpdateChannelHeader(data2); err == nil { t.Fatal("should have errored not channel admin") @@ -607,7 +606,7 @@ func TestUpdateChannelHeader(t *testing.T) { th.MakeUserChannelAdmin(th.BasicUser, channel2) th.MakeUserChannelAdmin(th.BasicUser, channel3) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() if _, err := Client.UpdateChannelHeader(data2); err != nil { t.Fatal(err) @@ -767,7 +766,7 @@ func TestUpdateChannelPurpose(t *testing.T) { }) th.MakeUserChannelUser(th.BasicUser, channel2) th.MakeUserChannelUser(th.BasicUser, channel3) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() if _, err := Client.UpdateChannelPurpose(data2); err == nil { t.Fatal("should have errored not channel admin") @@ -778,7 +777,7 @@ func TestUpdateChannelPurpose(t *testing.T) { th.MakeUserChannelAdmin(th.BasicUser, channel2) th.MakeUserChannelAdmin(th.BasicUser, channel3) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() if _, err := Client.UpdateChannelPurpose(data2); err != nil { t.Fatal(err) @@ -1344,7 +1343,7 @@ func TestDeleteChannel(t *testing.T) { th.MakeUserChannelAdmin(th.BasicUser, channel2) th.MakeUserChannelAdmin(th.BasicUser, channel3) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() if _, err := Client.DeleteChannel(channel2.Id); err != nil { t.Fatal(err) diff --git a/api/context.go b/api/context.go index a8ff2b694..1eb1e3f4f 100644 --- a/api/context.go +++ b/api/context.go @@ -364,10 +364,6 @@ func NewInvalidParamError(where string, name string) *model.AppError { return err } -func (c *Context) SetUnknownError(where string, details string) { - c.Err = model.NewAppError(where, "api.context.unknown.app_error", nil, details, http.StatusInternalServerError) -} - func (c *Context) SetPermissionError(permission *model.Permission) { c.Err = model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id, http.StatusForbidden) } @@ -387,11 +383,6 @@ func (c *Context) SetSiteURLHeader(url string) { c.siteURLHeader = strings.TrimRight(url, "/") } -// TODO see where these are used -func (c *Context) GetTeamURLFromTeam(team *model.Team) string { - return c.GetSiteURLHeader() + "/" + team.Name -} - func (c *Context) GetTeamURL() string { if !c.teamURLValid { c.SetTeamURLFromSession() @@ -406,10 +397,6 @@ func (c *Context) GetSiteURLHeader() string { return c.siteURLHeader } -func (c *Context) GetCurrentTeamMember() *model.TeamMember { - return c.Session.GetTeamByTeamId(c.TeamId) -} - func (c *Context) HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool { metrics := c.App.Metrics if et := r.Header.Get(model.HEADER_ETAG_CLIENT); len(etag) > 0 { diff --git a/api/post.go b/api/post.go index 192dc0abc..bed2f3fdb 100644 --- a/api/post.go +++ b/api/post.go @@ -136,10 +136,7 @@ func saveIsPinnedPost(c *Context, w http.ResponseWriter, r *http.Request, isPinn message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil) message.Add("post", c.App.PostWithProxyAddedToImageURLs(rpost).ToJson()) - - c.App.Go(func() { - c.App.Publish(message) - }) + c.App.Publish(message) c.App.InvalidateCacheForChannelPosts(rpost.ChannelId) diff --git a/api/websocket_test.go b/api/websocket_test.go index 0a39a012f..a3c716abd 100644 --- a/api/websocket_test.go +++ b/api/websocket_test.go @@ -227,7 +227,7 @@ func TestWebSocketEvent(t *testing.T) { } evt2 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", "somerandomid", "", nil) - go th.App.Publish(evt2) + th.App.Publish(evt2) time.Sleep(300 * time.Millisecond) eventHit = false diff --git a/api4/apitestlib.go b/api4/apitestlib.go index e55ca8c8b..6edd37812 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -467,6 +467,22 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) { utils.EnableDebugLogForTest() } +func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember { + utils.DisableDebugLogForTest() + + member, err := me.App.AddUserToChannel(user, channel) + if err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + + utils.EnableDebugLogForTest() + + return member +} + func (me *TestHelper) GenerateTestEmail() string { if me.App.Config().EmailSettings.SMTPServer != "dockerhost" && os.Getenv("CI_INBUCKET_PORT") == "" { return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com") @@ -510,18 +526,6 @@ func CheckUserSanitization(t *testing.T, user *model.User) { } } -func CheckTeamSanitization(t *testing.T, team *model.Team) { - t.Helper() - - if team.Email != "" { - t.Fatal("email wasn't blank") - } - - if team.AllowedDomains != "" { - t.Fatal("'allowed domains' wasn't blank") - } -} - func CheckEtag(t *testing.T, data interface{}, resp *model.Response) { t.Helper() @@ -669,21 +673,6 @@ func CheckInternalErrorStatus(t *testing.T, resp *model.Response) { } } -func CheckPayLoadTooLargeStatus(t *testing.T, resp *model.Response) { - t.Helper() - - if resp.Error == nil { - t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusRequestEntityTooLarge)) - return - } - - if resp.StatusCode != http.StatusRequestEntityTooLarge { - t.Log("actual: " + strconv.Itoa(resp.StatusCode)) - t.Log("expected: " + strconv.Itoa(http.StatusRequestEntityTooLarge)) - t.Fatal("wrong status code") - } -} - func readTestFile(name string) ([]byte, error) { path, _ := utils.FindDir("tests") file, err := os.Open(path + "/" + name) diff --git a/api4/channel_test.go b/api4/channel_test.go index e65918707..51c32cf71 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/store/sqlstore" ) func TestCreateChannel(t *testing.T) { @@ -909,7 +908,7 @@ func TestDeleteChannel(t *testing.T) { // successful delete by channel admin th.MakeUserChannelAdmin(user, publicChannel6) th.MakeUserChannelAdmin(user, privateChannel7) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() _, resp = Client.DeleteChannel(publicChannel6.Id) CheckNoError(t, resp) @@ -960,7 +959,7 @@ func TestDeleteChannel(t *testing.T) { // // cannot delete by channel admin th.MakeUserChannelAdmin(user, publicChannel6) th.MakeUserChannelAdmin(user, privateChannel7) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() _, resp = Client.DeleteChannel(publicChannel6.Id) CheckForbiddenStatus(t, resp) @@ -1001,7 +1000,7 @@ func TestDeleteChannel(t *testing.T) { // cannot delete by channel admin th.MakeUserChannelAdmin(user, publicChannel6) th.MakeUserChannelAdmin(user, privateChannel7) - sqlstore.ClearChannelCaches() + th.App.Srv.Store.Channel().ClearCaches() _, resp = Client.DeleteChannel(publicChannel6.Id) CheckForbiddenStatus(t, resp) diff --git a/api4/compliance.go b/api4/compliance.go index 71f0fa81d..4035afb77 100644 --- a/api4/compliance.go +++ b/api4/compliance.go @@ -7,8 +7,8 @@ import ( "net/http" "strconv" + "github.com/avct/uasurfer" "github.com/mattermost/mattermost-server/model" - "github.com/mssola/user_agent" ) func (api *API) InitCompliance() { @@ -108,12 +108,11 @@ func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer // attach extra headers to trigger a download on IE, Edge, and Safari - ua := user_agent.New(r.UserAgent()) - bname, _ := ua.Browser() + ua := uasurfer.Parse(r.UserAgent()) w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"") - if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" { + if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari { // trim off anything before the final / so we just get the file's name w.Header().Set("Content-Type", "application/octet-stream") } diff --git a/api4/system.go b/api4/system.go index 2355cb476..c1541f0b5 100644 --- a/api4/system.go +++ b/api4/system.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "runtime" - "strconv" l4g "github.com/alecthomas/log4go" "github.com/mattermost/mattermost-server/model" @@ -29,6 +28,7 @@ func (api *API) InitSystem() { api.BaseRoutes.ApiRoot.Handle("/audits", api.ApiSessionRequired(getAudits)).Methods("GET") api.BaseRoutes.ApiRoot.Handle("/email/test", api.ApiSessionRequired(testEmail)).Methods("POST") + api.BaseRoutes.ApiRoot.Handle("/file/s3_test", api.ApiSessionRequired(testS3)).Methods("POST") api.BaseRoutes.ApiRoot.Handle("/database/recycle", api.ApiSessionRequired(databaseRecycle)).Methods("POST") api.BaseRoutes.ApiRoot.Handle("/caches/invalidate", api.ApiSessionRequired(invalidateCaches)).Methods("POST") @@ -246,14 +246,7 @@ func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) { return } - respCfg := map[string]string{} - for k, v := range c.App.ClientConfig() { - respCfg[k] = v - } - - respCfg["NoAccounts"] = strconv.FormatBool(c.App.IsFirstUserAccount()) - - w.Write([]byte(model.MapToJson(respCfg))) + w.Write([]byte(model.MapToJson(c.App.ClientConfigWithNoAccounts()))) } func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { @@ -384,3 +377,33 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(rows.ToJson())) } + +func testS3(c *Context, w http.ResponseWriter, r *http.Request) { + cfg := model.ConfigFromJson(r.Body) + if cfg == nil { + cfg = c.App.Config() + } + + if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + err := utils.CheckMandatoryS3Fields(&cfg.FileSettings) + if err != nil { + c.Err = err + return + } + + license := c.App.License() + backend, appErr := utils.NewFileBackend(&cfg.FileSettings, license != nil && *license.Features.Compliance) + if appErr == nil { + appErr = backend.TestConnection() + } + if appErr != nil { + c.Err = appErr + return + } + + ReturnStatusOK(w) +} diff --git a/api4/system_test.go b/api4/system_test.go index 01b4934ae..e39486b77 100644 --- a/api4/system_test.go +++ b/api4/system_test.go @@ -1,7 +1,9 @@ package api4 import ( + "fmt" "net/http" + "os" "strings" "testing" @@ -466,3 +468,65 @@ func TestGetAnalyticsOld(t *testing.T) { _, resp = Client.GetAnalyticsOld("", th.BasicTeam.Id) CheckUnauthorizedStatus(t, resp) } + +func TestS3TestConnection(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + Client := th.Client + + s3Host := os.Getenv("CI_HOST") + if s3Host == "" { + s3Host = "dockerhost" + } + + s3Port := os.Getenv("CI_MINIO_PORT") + if s3Port == "" { + s3Port = "9001" + } + + s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port) + config := model.Config{ + FileSettings: model.FileSettings{ + DriverName: model.NewString(model.IMAGE_DRIVER_S3), + AmazonS3AccessKeyId: model.MINIO_ACCESS_KEY, + AmazonS3SecretAccessKey: model.MINIO_SECRET_KEY, + AmazonS3Bucket: "", + AmazonS3Endpoint: "", + AmazonS3SSL: model.NewBool(false), + }, + } + + _, resp := Client.TestS3Connection(&config) + CheckForbiddenStatus(t, resp) + + _, resp = th.SystemAdminClient.TestS3Connection(&config) + CheckBadRequestStatus(t, resp) + if resp.Error.Message != "S3 Bucket is required" { + t.Fatal("should return error - missing s3 bucket") + } + + config.FileSettings.AmazonS3Bucket = model.MINIO_BUCKET + _, resp = th.SystemAdminClient.TestS3Connection(&config) + CheckBadRequestStatus(t, resp) + if resp.Error.Message != "S3 Endpoint is required" { + t.Fatal("should return error - missing s3 endpoint") + } + + config.FileSettings.AmazonS3Endpoint = s3Endpoint + _, resp = th.SystemAdminClient.TestS3Connection(&config) + CheckBadRequestStatus(t, resp) + if resp.Error.Message != "S3 Region is required" { + t.Fatal("should return error - missing s3 region") + } + + config.FileSettings.AmazonS3Region = "us-east-1" + _, resp = th.SystemAdminClient.TestS3Connection(&config) + CheckOKStatus(t, resp) + + config.FileSettings.AmazonS3Bucket = "Wrong_bucket" + _, resp = th.SystemAdminClient.TestS3Connection(&config) + CheckInternalErrorStatus(t, resp) + if resp.Error.Message != "Error checking if bucket exists." { + t.Fatal("should return error ") + } +} diff --git a/api4/team.go b/api4/team.go index d770aee22..8e4c5c312 100644 --- a/api4/team.go +++ b/api4/team.go @@ -6,6 +6,7 @@ package api4 import ( "bytes" "encoding/base64" + "fmt" "net/http" "strconv" @@ -28,6 +29,10 @@ func (api *API) InitTeam() { api.BaseRoutes.Team.Handle("", api.ApiSessionRequired(deleteTeam)).Methods("DELETE") api.BaseRoutes.Team.Handle("/patch", api.ApiSessionRequired(patchTeam)).Methods("PUT") api.BaseRoutes.Team.Handle("/stats", api.ApiSessionRequired(getTeamStats)).Methods("GET") + + api.BaseRoutes.Team.Handle("/image", api.ApiSessionRequiredTrustRequester(getTeamIcon)).Methods("GET") + api.BaseRoutes.Team.Handle("/image", api.ApiSessionRequired(setTeamIcon)).Methods("POST") + api.BaseRoutes.TeamMembers.Handle("", api.ApiSessionRequired(getTeamMembers)).Methods("GET") api.BaseRoutes.TeamMembers.Handle("/ids", api.ApiSessionRequired(getTeamMembersByIds)).Methods("POST") api.BaseRoutes.TeamMembersForUser.Handle("", api.ApiSessionRequired(getTeamMembersForUser)).Methods("GET") @@ -729,3 +734,81 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } } + +func getTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_VIEW_TEAM) { + c.SetPermissionError(model.PERMISSION_VIEW_TEAM) + return + } + + if team, err := c.App.GetTeam(c.Params.TeamId); err != nil { + c.Err = err + return + } else { + etag := strconv.FormatInt(team.LastTeamIconUpdate, 10) + + if c.HandleEtag(etag, "Get Team Icon", w, r) { + return + } + + if img, err := c.App.GetTeamIcon(team); err != nil { + c.Err = err + return + } else { + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, public", 24*60*60)) // 24 hrs + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + w.Write(img) + } + } +} + +func setTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_MANAGE_TEAM) { + c.SetPermissionError(model.PERMISSION_MANAGE_TEAM) + return + } + + if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize { + c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, "", http.StatusBadRequest) + return + } + + if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil { + c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.parse.app_error", nil, err.Error(), http.StatusBadRequest) + return + } + + m := r.MultipartForm + + imageArray, ok := m.File["image"] + if !ok { + c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.no_file.app_error", nil, "", http.StatusBadRequest) + return + } + + if len(imageArray) <= 0 { + c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.array.app_error", nil, "", http.StatusBadRequest) + return + } + + imageData := imageArray[0] + + if err := c.App.SetTeamIcon(c.Params.TeamId, imageData); err != nil { + c.Err = err + return + } + + c.LogAudit("") + ReturnStatusOK(w) +} diff --git a/api4/team_test.go b/api4/team_test.go index faa90e511..04a0e9ae4 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -15,6 +15,8 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCreateTeam(t *testing.T) { @@ -1915,3 +1917,82 @@ func TestGetTeamInviteInfo(t *testing.T) { _, resp = Client.GetTeamInviteInfo("junk") CheckNotFoundStatus(t, resp) } + +func TestSetTeamIcon(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + Client := th.Client + team := th.BasicTeam + + data, err := readTestFile("test.png") + if err != nil { + t.Fatal(err) + } + + th.LoginTeamAdmin() + + ok, resp := Client.SetTeamIcon(team.Id, data) + if !ok { + t.Fatal(resp.Error) + } + CheckNoError(t, resp) + + ok, resp = Client.SetTeamIcon(model.NewId(), data) + if ok { + t.Fatal("Should return false, set team icon not allowed") + } + CheckForbiddenStatus(t, resp) + + th.LoginBasic() + + _, resp = Client.SetTeamIcon(team.Id, data) + if resp.StatusCode == http.StatusForbidden { + CheckForbiddenStatus(t, resp) + } else if resp.StatusCode == http.StatusUnauthorized { + CheckUnauthorizedStatus(t, resp) + } else { + t.Fatal("Should have failed either forbidden or unauthorized") + } + + Client.Logout() + + _, resp = Client.SetTeamIcon(team.Id, data) + if resp.StatusCode == http.StatusForbidden { + CheckForbiddenStatus(t, resp) + } else if resp.StatusCode == http.StatusUnauthorized { + CheckUnauthorizedStatus(t, resp) + } else { + t.Fatal("Should have failed either forbidden or unauthorized") + } + + teamBefore, err := th.App.GetTeam(team.Id) + require.Nil(t, err) + + _, resp = th.SystemAdminClient.SetTeamIcon(team.Id, data) + CheckNoError(t, resp) + + teamAfter, err := th.App.GetTeam(team.Id) + require.Nil(t, err) + assert.True(t, teamBefore.LastTeamIconUpdate < teamAfter.LastTeamIconUpdate, "LastTeamIconUpdate should have been updated for team") + + info := &model.FileInfo{Path: "teams/" + team.Id + "/teamIcon.png"} + if err := th.cleanupTestFile(info); err != nil { + t.Fatal(err) + } +} + +func TestGetTeamIcon(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + Client := th.Client + team := th.BasicTeam + + // should always fail because no initial image and no auto creation + _, resp := Client.GetTeamIcon(team.Id, "") + CheckNotFoundStatus(t, resp) + + Client.Logout() + + _, resp = Client.GetTeamIcon(team.Id, "") + CheckUnauthorizedStatus(t, resp) +} diff --git a/api4/user.go b/api4/user.go index f82a6e3d5..8f8f08c75 100644 --- a/api4/user.go +++ b/api4/user.go @@ -290,16 +290,21 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - if sort != "" && sort != "last_activity_at" && sort != "create_at" { + if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" { c.SetInvalidUrlParam("sort") return } // Currently only supports sorting on a team + // or sort="status" on inChannelId if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") { c.SetInvalidUrlParam("sort") return } + if sort == "status" && inChannelId == "" { + c.SetInvalidUrlParam("sort") + return + } var profiles []*model.User var err *model.AppError @@ -355,8 +360,11 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PERMISSION_READ_CHANNEL) return } - - profiles, err = c.App.GetUsersInChannelPage(inChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + if sort == "status" { + profiles, err = c.App.GetUsersInChannelPageByStatus(inChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + } else { + profiles, err = c.App.GetUsersInChannelPage(inChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + } } else { // No permission check required diff --git a/api4/user_test.go b/api4/user_test.go index 4613a8ea9..f04cd6ab2 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -2650,3 +2650,146 @@ func TestUserAccessTokenDisableConfig(t *testing.T) { _, resp = Client.GetMe("") CheckNoError(t, resp) } + +func TestGetUsersByStatus(t *testing.T) { + th := Setup() + defer th.TearDown() + + team, err := th.App.CreateTeam(&model.Team{ + DisplayName: "dn_" + model.NewId(), + Name: GenerateTestTeamName(), + Email: th.GenerateTestEmail(), + Type: model.TEAM_OPEN, + }) + if err != nil { + t.Fatalf("failed to create team: %v", err) + } + + channel, err := th.App.CreateChannel(&model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: "name_" + model.NewId(), + Type: model.CHANNEL_OPEN, + TeamId: team.Id, + CreatorId: model.NewId(), + }, false) + if err != nil { + t.Fatalf("failed to create channel: %v", err) + } + + createUserWithStatus := func(username string, status string) *model.User { + id := model.NewId() + + user, err := th.App.CreateUser(&model.User{ + Email: "success+" + id + "@simulator.amazonses.com", + Username: "un_" + username + "_" + id, + Nickname: "nn_" + id, + Password: "Password1", + }) + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + + th.LinkUserToTeam(user, team) + th.AddUserToChannel(user, channel) + + th.App.SaveAndBroadcastStatus(&model.Status{ + UserId: user.Id, + Status: status, + Manual: true, + }) + + return user + } + + // Creating these out of order in case that affects results + offlineUser1 := createUserWithStatus("offline1", model.STATUS_OFFLINE) + offlineUser2 := createUserWithStatus("offline2", model.STATUS_OFFLINE) + awayUser1 := createUserWithStatus("away1", model.STATUS_AWAY) + awayUser2 := createUserWithStatus("away2", model.STATUS_AWAY) + onlineUser1 := createUserWithStatus("online1", model.STATUS_ONLINE) + onlineUser2 := createUserWithStatus("online2", model.STATUS_ONLINE) + dndUser1 := createUserWithStatus("dnd1", model.STATUS_DND) + dndUser2 := createUserWithStatus("dnd2", model.STATUS_DND) + + client := th.CreateClient() + if _, resp := client.Login(onlineUser2.Username, "Password1"); resp.Error != nil { + t.Fatal(resp.Error) + } + + t.Run("sorting by status then alphabetical", func(t *testing.T) { + usersByStatus, resp := client.GetUsersInChannelByStatus(channel.Id, 0, 8, "") + if resp.Error != nil { + t.Fatal(resp.Error) + } + + expectedUsersByStatus := []*model.User{ + onlineUser1, + onlineUser2, + awayUser1, + awayUser2, + dndUser1, + dndUser2, + offlineUser1, + offlineUser2, + } + + if len(usersByStatus) != len(expectedUsersByStatus) { + t.Fatalf("received only %v users, expected %v", len(usersByStatus), len(expectedUsersByStatus)) + } + + for i := range usersByStatus { + if usersByStatus[i].Id != expectedUsersByStatus[i].Id { + t.Fatalf("received user %v at index %v, expected %v", usersByStatus[i].Username, i, expectedUsersByStatus[i].Username) + } + } + }) + + t.Run("paging", func(t *testing.T) { + usersByStatus, resp := client.GetUsersInChannelByStatus(channel.Id, 0, 3, "") + if resp.Error != nil { + t.Fatal(resp.Error) + } + + if len(usersByStatus) != 3 { + t.Fatal("received too many users") + } + + if usersByStatus[0].Id != onlineUser1.Id && usersByStatus[1].Id != onlineUser2.Id { + t.Fatal("expected to receive online users first") + } + + if usersByStatus[2].Id != awayUser1.Id { + t.Fatal("expected to receive away users second") + } + + usersByStatus, resp = client.GetUsersInChannelByStatus(channel.Id, 1, 3, "") + if resp.Error != nil { + t.Fatal(resp.Error) + } + + if usersByStatus[0].Id != awayUser2.Id { + t.Fatal("expected to receive away users second") + } + + if usersByStatus[1].Id != dndUser1.Id && usersByStatus[2].Id != dndUser2.Id { + t.Fatal("expected to receive dnd users third") + } + + usersByStatus, resp = client.GetUsersInChannelByStatus(channel.Id, 1, 4, "") + if resp.Error != nil { + t.Fatal(resp.Error) + } + + if len(usersByStatus) != 4 { + t.Fatal("received too many users") + } + + if usersByStatus[0].Id != dndUser1.Id && usersByStatus[1].Id != dndUser2.Id { + t.Fatal("expected to receive dnd users third") + } + + if usersByStatus[2].Id != offlineUser1.Id && usersByStatus[3].Id != offlineUser2.Id { + t.Fatal("expected to receive offline users last") + } + }) +} diff --git a/api4/webhook.go b/api4/webhook.go index e19f14704..853cf43f3 100644 --- a/api4/webhook.go +++ b/api4/webhook.go @@ -510,7 +510,6 @@ func commandWebhook(c *Context, w http.ResponseWriter, r *http.Request) { } func decodePayload(payload io.Reader) (*model.IncomingWebhookRequest, *model.AppError) { - decodeError := &model.AppError{} incomingWebhookPayload, decodeError := model.IncomingWebhookRequestFromJson(payload) if decodeError != nil { diff --git a/app/admin.go b/app/admin.go index 154fa8899..22928390e 100644 --- a/app/admin.go +++ b/app/admin.go @@ -15,7 +15,6 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/store/sqlstore" "github.com/mattermost/mattermost-server/utils" ) @@ -141,10 +140,11 @@ func (a *App) InvalidateAllCachesSkipSend() { l4g.Info(utils.T("api.context.invalidate_all_caches")) a.sessionCache.Purge() ClearStatusCache() - sqlstore.ClearChannelCaches() - sqlstore.ClearUserCaches() - sqlstore.ClearPostCaches() - sqlstore.ClearWebhookCaches() + a.Srv.Store.Channel().ClearCaches() + a.Srv.Store.User().ClearCaches() + a.Srv.Store.Post().ClearCaches() + a.Srv.Store.FileInfo().ClearCaches() + a.Srv.Store.Webhook().ClearCaches() a.LoadLicense() } diff --git a/app/app.go b/app/app.go index 26aed4c73..f5e5dd21e 100644 --- a/app/app.go +++ b/app/app.go @@ -131,8 +131,24 @@ func New(options ...Option) (outApp *App, outErr error) { app.configListenerId = app.AddConfigListener(func(_, _ *model.Config) { app.configOrLicenseListener() + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CONFIG_CHANGED, "", "", "", nil) + + message.Add("config", app.ClientConfigWithNoAccounts()) + app.Go(func() { + app.Publish(message) + }) + }) + app.licenseListenerId = app.AddLicenseListener(func() { + app.configOrLicenseListener() + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LICENSE_CHANGED, "", "", "", nil) + message.Add("license", app.GetSanitizedClientLicense()) + app.Go(func() { + app.Publish(message) + }) + }) - app.licenseListenerId = app.AddLicenseListener(app.configOrLicenseListener) app.regenerateClientConfig() app.setDefaultRolesBasedOnConfig() diff --git a/app/apptestlib.go b/app/apptestlib.go index c7846c9b5..01f5b0102 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -135,10 +135,6 @@ func (me *TestHelper) InitBasic() *TestHelper { return me } -func (me *TestHelper) MakeUsername() string { - return "un_" + model.NewId() -} - func (me *TestHelper) MakeEmail() string { return "success_" + model.NewId() + "@simulator.amazonses.com" } @@ -191,10 +187,6 @@ func (me *TestHelper) CreateChannel(team *model.Team) *model.Channel { return me.createChannel(team, model.CHANNEL_OPEN) } -func (me *TestHelper) CreatePrivateChannel(team *model.Team) *model.Channel { - return me.createChannel(team, model.CHANNEL_PRIVATE) -} - func (me *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel { id := model.NewId() @@ -253,6 +245,22 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) { utils.EnableDebugLogForTest() } +func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember { + utils.DisableDebugLogForTest() + + member, err := me.App.AddUserToChannel(user, channel) + if err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + + utils.EnableDebugLogForTest() + + return member +} + func (me *TestHelper) TearDown() { me.App.Shutdown() os.Remove(me.tempConfigPath) diff --git a/app/authorization.go b/app/authorization.go index 3a64bb717..4231cac77 100644 --- a/app/authorization.go +++ b/app/authorization.go @@ -181,18 +181,6 @@ func (a *App) HasPermissionToChannelByPost(askingUserId string, postId string, p return a.HasPermissionTo(askingUserId, permission) } -func (a *App) HasPermissionToUser(askingUserId string, userId string) bool { - if askingUserId == userId { - return true - } - - if a.HasPermissionTo(askingUserId, model.PERMISSION_EDIT_OTHER_USERS) { - return true - } - - return false -} - func (a *App) CheckIfRolesGrantPermission(roles []string, permissionId string) bool { for _, roleId := range roles { if role := a.Role(roleId); role == nil { diff --git a/app/auto_constants.go b/app/auto_constants.go index c52eb6243..520d4e363 100644 --- a/app/auto_constants.go +++ b/app/auto_constants.go @@ -9,16 +9,15 @@ import ( ) const ( - USER_PASSWORD = "passwd" - CHANNEL_TYPE = model.CHANNEL_OPEN - FUZZ_USER_EMAIL_PREFIX_LEN = 10 - BTEST_TEAM_DISPLAY_NAME = "TestTeam" - BTEST_TEAM_NAME = "z-z-testdomaina" - BTEST_TEAM_EMAIL = "test@nowhere.com" - BTEST_TEAM_TYPE = model.TEAM_OPEN - BTEST_USER_NAME = "Mr. Testing Tester" - BTEST_USER_EMAIL = "success+ttester@simulator.amazonses.com" - BTEST_USER_PASSWORD = "passwd" + USER_PASSWORD = "passwd" + CHANNEL_TYPE = model.CHANNEL_OPEN + BTEST_TEAM_DISPLAY_NAME = "TestTeam" + BTEST_TEAM_NAME = "z-z-testdomaina" + BTEST_TEAM_EMAIL = "test@nowhere.com" + BTEST_TEAM_TYPE = model.TEAM_OPEN + BTEST_USER_NAME = "Mr. Testing Tester" + BTEST_USER_EMAIL = "success+ttester@simulator.amazonses.com" + BTEST_USER_PASSWORD = "passwd" ) var ( @@ -29,8 +28,5 @@ var ( USER_EMAIL_LEN = utils.Range{Begin: 15, End: 30} CHANNEL_DISPLAY_NAME_LEN = utils.Range{Begin: 10, End: 20} CHANNEL_NAME_LEN = utils.Range{Begin: 5, End: 20} - POST_MESSAGE_LEN = utils.Range{Begin: 100, End: 400} - POST_HASHTAGS_NUM = utils.Range{Begin: 5, End: 10} - POST_MENTIONS_NUM = utils.Range{Begin: 0, End: 3} TEST_IMAGE_FILENAMES = []string{"test.png", "testjpg.jpg", "testgif.gif"} ) diff --git a/app/auto_posts.go b/app/auto_posts.go index 6d1e352e5..379c74ab7 100644 --- a/app/auto_posts.go +++ b/app/auto_posts.go @@ -90,18 +90,3 @@ func (cfg *AutoPostCreator) CreateRandomPost() (*model.Post, bool) { } return result.Data.(*model.Post), true } - -func (cfg *AutoPostCreator) CreateTestPosts(rangePosts utils.Range) ([]*model.Post, bool) { - numPosts := utils.RandIntFromRange(rangePosts) - posts := make([]*model.Post, numPosts) - - for i := 0; i < numPosts; i++ { - var err bool - posts[i], err = cfg.CreateRandomPost() - if !err { - return posts, false - } - } - - return posts, true -} diff --git a/app/channel.go b/app/channel.go index 8ac1f421c..4e294abbb 100644 --- a/app/channel.go +++ b/app/channel.go @@ -225,6 +225,14 @@ func (a *App) createDirectChannel(userId string, otherUserId string) (*model.Cha } } else { channel := result.Data.(*model.Channel) + + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(otherUserId, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + return channel, nil } } @@ -369,7 +377,7 @@ func (a *App) postChannelPrivacyMessage(user *model.User, channel *model.Channel })[channel.Type] post := &model.Post{ ChannelId: channel.Id, - Message: fmt.Sprintf(utils.T("api.channel.change_channel_privacy." + privacy)), + Message: utils.T("api.channel.change_channel_privacy." + privacy), Type: model.POST_CHANGE_CHANNEL_PRIVACY, UserId: user.Id, Props: model.StringInterface{ @@ -545,7 +553,6 @@ func (a *App) DeleteChannel(channel *model.Channel, userId string) *model.AppErr message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, channel.TeamId, "", "", nil) message.Add("channel_id", channel.Id) - a.Publish(message) } @@ -1055,7 +1062,7 @@ func (a *App) LeaveChannel(channelId string, userId string) *model.AppError { return err } - if channel.Name == model.DEFAULT_CHANNEL && *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages == false { + if channel.Name == model.DEFAULT_CHANNEL && !*a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { return nil } @@ -1093,7 +1100,9 @@ func (a *App) PostAddToChannelMessage(user *model.User, addedUser *model.User, c UserId: user.Id, RootId: postRootId, Props: model.StringInterface{ + "userId": user.Id, "username": user.Username, + "addedUserId": addedUser.Id, "addedUsername": addedUser.Username, }, } @@ -1113,7 +1122,9 @@ func (a *App) postAddToTeamMessage(user *model.User, addedUser *model.User, chan UserId: user.Id, RootId: postRootId, Props: model.StringInterface{ + "userId": user.Id, "username": user.Username, + "addedUserId": addedUser.Id, "addedUsername": addedUser.Username, }, } @@ -1132,6 +1143,7 @@ func (a *App) postRemoveFromChannelMessage(removerUserId string, removedUser *mo Type: model.POST_REMOVE_FROM_CHANNEL, UserId: removerUserId, Props: model.StringInterface{ + "removedUserId": removedUser.Id, "removedUsername": removedUser.Username, }, } @@ -1166,17 +1178,13 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string, message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil) message.Add("user_id", userIdToRemove) message.Add("remover_id", removerUserId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) // because the removed user no longer belongs to the channel we need to send a separate websocket event userMsg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", "", userIdToRemove, nil) userMsg.Add("channel_id", channel.Id) userMsg.Add("remover_id", removerUserId) - a.Go(func() { - a.Publish(userMsg) - }) + a.Publish(userMsg) return nil } @@ -1246,9 +1254,7 @@ func (a *App) UpdateChannelLastViewedAt(channelIds []string, userId string) *mod for _, channelId := range channelIds { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, "", "", userId, nil) message.Add("channel_id", channelId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } } @@ -1325,9 +1331,7 @@ func (a *App) ViewChannel(view *model.ChannelView, userId string, clearPushNotif if *a.Config().ServiceSettings.EnableChannelViewedMessages && model.IsValidId(view.ChannelId) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, "", "", userId, nil) message.Add("channel_id", view.ChannelId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } return times, nil @@ -1430,7 +1434,16 @@ func (a *App) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model. } a.InvalidateCacheForUser(userId1) a.InvalidateCacheForUser(userId2) - return result.Data.(*model.Channel), nil + + channel := result.Data.(*model.Channel) + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId1, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId2, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + + return channel, nil } else if result.Err != nil { return nil, model.NewAppError("GetOrCreateDMChannel", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode) } diff --git a/app/channel_test.go b/app/channel_test.go index e4a0e4320..69efaeca7 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -110,7 +110,7 @@ func TestMoveChannel(t *testing.T) { } } -func TestJoinDefaultChannelsTownSquare(t *testing.T) { +func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordTownSquare(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -136,7 +136,7 @@ func TestJoinDefaultChannelsTownSquare(t *testing.T) { assert.True(t, found) } -func TestJoinDefaultChannelsOffTopic(t *testing.T) { +func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordOffTopic(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -162,7 +162,7 @@ func TestJoinDefaultChannelsOffTopic(t *testing.T) { assert.True(t, found) } -func TestCreateChannelPublic(t *testing.T) { +func TestCreateChannelPublicCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -176,7 +176,7 @@ func TestCreateChannelPublic(t *testing.T) { assert.Equal(t, publicChannel.Id, histories[0].ChannelId) } -func TestCreateChannelPrivate(t *testing.T) { +func TestCreateChannelPrivateCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -205,7 +205,7 @@ func TestUpdateChannelPrivacy(t *testing.T) { } } -func TestCreateGroupChannel(t *testing.T) { +func TestCreateGroupChannelCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -233,7 +233,62 @@ func TestCreateGroupChannel(t *testing.T) { } } -func TestAddUserToChannel(t *testing.T) { +func TestCreateDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user1 := th.CreateUser() + user2 := th.CreateUser() + + if channel, err := th.App.CreateDirectChannel(user1.Id, user2.Id); err != nil { + t.Fatal("Failed to create direct channel. Error: " + err.Message) + } else { + // there should be a ChannelMemberHistory record for both users + histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult) + assert.Len(t, histories, 2) + + historyId0 := histories[0].UserId + historyId1 := histories[1].UserId + switch historyId0 { + case user1.Id: + assert.Equal(t, user2.Id, historyId1) + case user2.Id: + assert.Equal(t, user1.Id, historyId1) + default: + t.Fatal("Unexpected user id " + historyId0 + " in ChannelMemberHistory table") + } + } +} + +func TestGetDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user1 := th.CreateUser() + user2 := th.CreateUser() + + // this function call implicitly creates a direct channel between the two users if one doesn't already exist + if channel, err := th.App.GetDirectChannel(user1.Id, user2.Id); err != nil { + t.Fatal("Failed to create direct channel. Error: " + err.Message) + } else { + // there should be a ChannelMemberHistory record for both users + histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult) + assert.Len(t, histories, 2) + + historyId0 := histories[0].UserId + historyId1 := histories[1].UserId + switch historyId0 { + case user1.Id: + assert.Equal(t, user2.Id, historyId1) + case user2.Id: + assert.Equal(t, user1.Id, historyId1) + default: + t.Fatal("Unexpected user id " + historyId0 + " in ChannelMemberHistory table") + } + } +} + +func TestAddUserToChannelCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -263,7 +318,7 @@ func TestAddUserToChannel(t *testing.T) { assert.Equal(t, groupUserIds, channelMemberHistoryUserIds) } -func TestRemoveUserFromChannel(t *testing.T) { +func TestRemoveUserFromChannelUpdatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/command_expand_collapse.go b/app/command_expand_collapse.go index a8eb3bc1f..638490c6c 100644 --- a/app/command_expand_collapse.go +++ b/app/command_expand_collapse.go @@ -74,9 +74,7 @@ func (a *App) setCollapsePreference(args *model.CommandArgs, isCollapse bool) *m socketMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", args.UserId, nil) socketMessage.Add("preference", pref.ToJson()) - a.Go(func() { - a.Publish(socketMessage) - }) + a.Publish(socketMessage) var rmsg string diff --git a/app/config.go b/app/config.go index 35a0c9a3f..460d580d8 100644 --- a/app/config.go +++ b/app/config.go @@ -14,6 +14,7 @@ import ( "fmt" "net/url" "runtime/debug" + "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -34,6 +35,7 @@ func (a *App) UpdateConfig(f func(*model.Config)) { updated := old.Clone() f(updated) a.config.Store(updated) + a.InvokeConfigListeners(old, updated) } @@ -269,3 +271,16 @@ func (a *App) GetCookieDomain() string { func (a *App) GetSiteURL() string { return a.siteURL } + +// ClientConfigWithNoAccounts gets the configuration in a format suitable for sending to the client. +func (a *App) ClientConfigWithNoAccounts() map[string]string { + respCfg := map[string]string{} + for k, v := range a.ClientConfig() { + respCfg[k] = v + } + + // NoAccounts is not actually part of the configuration, but is expected by the client. + respCfg["NoAccounts"] = strconv.FormatBool(a.IsFirstUserAccount()) + + return respCfg +} diff --git a/app/config_test.go b/app/config_test.go index 5ee999f0f..051fa8fd8 100644 --- a/app/config_test.go +++ b/app/config_test.go @@ -63,3 +63,13 @@ func TestAsymmetricSigningKey(t *testing.T) { assert.NotNil(t, th.App.AsymmetricSigningKey()) assert.NotEmpty(t, th.App.ClientConfig()["AsymmetricSigningPublicKey"]) } + +func TestClientConfigWithNoAccounts(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + config := th.App.ClientConfigWithNoAccounts() + if _, ok := config["NoAccounts"]; !ok { + t.Fatal("expected NoAccounts in returned config") + } +} diff --git a/app/diagnostics.go b/app/diagnostics.go index 12553afc8..4cff5f02a 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -502,11 +502,15 @@ func (a *App) trackConfig() { }) a.SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{ - "enable_message_export": *cfg.MessageExportSettings.EnableExport, - "export_format": *cfg.MessageExportSettings.ExportFormat, - "daily_run_time": *cfg.MessageExportSettings.DailyRunTime, - "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp, - "batch_size": *cfg.MessageExportSettings.BatchSize, + "enable_message_export": *cfg.MessageExportSettings.EnableExport, + "export_format": *cfg.MessageExportSettings.ExportFormat, + "daily_run_time": *cfg.MessageExportSettings.DailyRunTime, + "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp, + "batch_size": *cfg.MessageExportSettings.BatchSize, + "global_relay_customer_type": *cfg.MessageExportSettings.GlobalRelaySettings.CustomerType, + "is_default_global_relay_smtp_username": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SmtpUsername, ""), + "is_default_global_relay_smtp_password": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SmtpPassword, ""), + "is_default_global_relay_email_address": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.EmailAddress, ""), }) } diff --git a/app/emoji.go b/app/emoji.go index 20d4bb44d..eebe59ccf 100644 --- a/app/emoji.go +++ b/app/emoji.go @@ -60,7 +60,6 @@ func (a *App) CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartIma } else { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EMOJI_ADDED, "", "", "", nil) message.Add("emoji", emoji.ToJson()) - a.Publish(message) return result.Data.(*model.Emoji), nil } diff --git a/app/login.go b/app/login.go index e01566bcd..43b022749 100644 --- a/app/login.go +++ b/app/login.go @@ -9,8 +9,8 @@ import ( "strings" "time" + "github.com/avct/uasurfer" "github.com/mattermost/mattermost-server/model" - "github.com/mssola/user_agent" ) func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId string, ldapOnly bool) (*model.User, *model.AppError) { @@ -71,19 +71,19 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, session.SetExpireInDays(*a.Config().ServiceSettings.SessionLengthWebInDays) } - ua := user_agent.New(r.UserAgent()) + ua := uasurfer.Parse(r.UserAgent()) - plat := ua.Platform() + plat := ua.OS.Platform.String() if plat == "" { plat = "unknown" } - os := ua.OS() + os := ua.OS.Name.String() if os == "" { os = "unknown" } - bname, bversion := ua.Browser() + bname := ua.Browser.Name.String() if bname == "" { bname = "unknown" } @@ -92,9 +92,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, bname = "Desktop App" } - if bversion == "" { - bversion = "0.0" - } + bversion := ua.Browser.Version session.AddProp(model.SESSION_PROP_PLATFORM, plat) session.AddProp(model.SESSION_PROP_OS, os) diff --git a/app/post.go b/app/post.go index a541797fa..5067777ab 100644 --- a/app/post.go +++ b/app/post.go @@ -84,9 +84,7 @@ func (a *App) CreatePostAsUser(post *model.Post) (*model.Post, *model.AppError) if *a.Config().ServiceSettings.EnableChannelViewedMessages { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, "", "", post.UserId, nil) message.Add("channel_id", post.ChannelId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } } @@ -314,10 +312,7 @@ func (a *App) SendEphemeralPost(userId string, post *model.Post) *model.Post { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) - - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return post } @@ -424,10 +419,7 @@ func (a *App) PatchPost(postId string, patch *model.PostPatch) (*model.Post, *mo func (a *App) sendUpdatedPostEvent(post *model.Post) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) - - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) GetPostsPage(channelId string, page int, perPage int) (*model.PostList, *model.AppError) { @@ -567,11 +559,9 @@ func (a *App) DeletePost(postId string) (*model.Post, *model.AppError) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil) message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) + a.Publish(message) a.Go(func() { - a.Publish(message) - }) - a.Go(func() { a.DeletePostFiles(post) }) a.Go(func() { diff --git a/app/preference.go b/app/preference.go index 9ca1f474c..eb41992da 100644 --- a/app/preference.go +++ b/app/preference.go @@ -55,9 +55,7 @@ func (a *App) UpdatePreferences(userId string, preferences model.Preferences) *m message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCES_CHANGED, "", "", userId, nil) message.Add("preferences", preferences.ToJson()) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return nil } @@ -80,9 +78,7 @@ func (a *App) DeletePreferences(userId string, preferences model.Preferences) *m message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCES_DELETED, "", "", userId, nil) message.Add("preferences", preferences.ToJson()) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return nil } diff --git a/app/server.go b/app/server.go index 93804a372..0c6c25ba5 100644 --- a/app/server.go +++ b/app/server.go @@ -84,28 +84,6 @@ func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second -type VaryBy struct { - useIP bool - useAuth bool -} - -func (m *VaryBy) Key(r *http.Request) string { - key := "" - - if m.useAuth { - token, tokenLocation := ParseAuthTokenFromRequest(r) - if tokenLocation != TokenLocationNotFound { - key += token - } else if m.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP - key += utils.GetIpAddress(r) - } - } else if m.useIP { // Only if Auth based is not enabed do we use a plain IP based - key = utils.GetIpAddress(r) - } - - return key -} - func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) { if r.Host == "" { http.Error(w, "Not Found", http.StatusNotFound) @@ -223,31 +201,6 @@ func (a *App) StartServer() error { return nil } -type tcpKeepAliveListener struct { - *net.TCPListener -} - -func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { - tc, err := ln.AcceptTCP() - if err != nil { - return - } - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - return tc, nil -} - -func (a *App) Listen(addr string) (net.Listener, error) { - if addr == "" { - addr = ":http" - } - ln, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil -} - func (a *App) StopServer() { if a.Srv.Server != nil { ctx, cancel := context.WithTimeout(context.Background(), TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN) diff --git a/app/server_test.go b/app/server_test.go index de358b976..94771a44e 100644 --- a/app/server_test.go +++ b/app/server_test.go @@ -26,7 +26,7 @@ func TestStartServerRateLimiterCriticalError(t *testing.T) { // Attempt to use Rate Limiter with an invalid config a.UpdateConfig(func(cfg *model.Config) { - *cfg.RateLimitSettings.Enable = true + *cfg.RateLimitSettings.Enable = true *cfg.RateLimitSettings.MaxBurst = -100 }) diff --git a/app/session.go b/app/session.go index 459618439..88f52477f 100644 --- a/app/session.go +++ b/app/session.go @@ -138,6 +138,9 @@ func (a *App) ClearSessionCacheForUserSkipClusterSend(userId string) { session := ts.(*model.Session) if session.UserId == userId { a.sessionCache.Remove(key) + if a.Metrics != nil { + a.Metrics.IncrementMemCacheInvalidationCounterSession() + } } } } diff --git a/app/slackimport.go b/app/slackimport.go index 9d1b4cf9c..ed522671a 100644 --- a/app/slackimport.go +++ b/app/slackimport.go @@ -109,13 +109,11 @@ func SlackParseUsers(data io.Reader) ([]SlackUser, error) { decoder := json.NewDecoder(data) var users []SlackUser - if err := decoder.Decode(&users); err != nil { - // This actually returns errors that are ignored. - // In this case it is erroring because of a null that Slack - // introduced. So we just return the users here. - return users, err - } - return users, nil + err := decoder.Decode(&users) + // This actually returns errors that are ignored. + // In this case it is erroring because of a null that Slack + // introduced. So we just return the users here. + return users, err } func SlackParsePosts(data io.Reader) ([]SlackPost, error) { diff --git a/app/status.go b/app/status.go index 1ef7aef0f..c8bff0d1a 100644 --- a/app/status.go +++ b/app/status.go @@ -221,9 +221,7 @@ func (a *App) BroadcastStatus(status *model.Status) { event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) event.Add("status", status.Status) event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.Publish(event) } func (a *App) SetStatusOffline(userId string, manual bool) { @@ -238,18 +236,7 @@ func (a *App) SetStatusOffline(userId string, manual bool) { status = &model.Status{UserId: userId, Status: model.STATUS_OFFLINE, Manual: manual, LastActivityAt: model.GetMillis(), ActiveChannel: ""} - a.AddStatusCache(status) - - if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) - } - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_OFFLINE) - event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.SaveAndBroadcastStatus(status) } func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) { @@ -281,18 +268,7 @@ func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) { status.Manual = manual status.ActiveChannel = "" - a.AddStatusCache(status) - - if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) - } - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_AWAY) - event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.SaveAndBroadcastStatus(status) } func (a *App) SetStatusDoNotDisturb(userId string) { @@ -309,18 +285,22 @@ func (a *App) SetStatusDoNotDisturb(userId string) { status.Status = model.STATUS_DND status.Manual = true + a.SaveAndBroadcastStatus(status) +} + +func (a *App) SaveAndBroadcastStatus(status *model.Status) *model.AppError { a.AddStatusCache(status) if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) + l4g.Error(utils.T("api.status.save_status.error"), status.UserId, result.Err) } event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_DND) + event.Add("status", status.Status) event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.Publish(event) + + return nil } func GetStatusFromCache(userId string) *model.Status { diff --git a/app/status_test.go b/app/status_test.go new file mode 100644 index 000000000..bf5736a48 --- /dev/null +++ b/app/status_test.go @@ -0,0 +1,40 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" +) + +func TestSaveStatus(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := th.BasicUser + + for _, statusString := range []string{ + model.STATUS_ONLINE, + model.STATUS_AWAY, + model.STATUS_DND, + model.STATUS_OFFLINE, + } { + t.Run(statusString, func(t *testing.T) { + status := &model.Status{ + UserId: user.Id, + Status: statusString, + } + + th.App.SaveAndBroadcastStatus(status) + + after, err := th.App.GetStatus(user.Id) + if err != nil { + t.Fatalf("failed to get status after save: %v", err) + } else if after.Status != statusString { + t.Fatalf("failed to save status, got %v, expected %v", after.Status, statusString) + } + }) + } +} diff --git a/app/team.go b/app/team.go index d8750bfbb..239ce4369 100644 --- a/app/team.go +++ b/app/team.go @@ -4,13 +4,18 @@ package app import ( + "bytes" "fmt" + "image" + "image/png" + "mime/multipart" "net/http" "net/url" "strconv" "strings" l4g "github.com/alecthomas/log4go" + "github.com/disintegration/imaging" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" @@ -134,9 +139,7 @@ func (a *App) sendTeamEvent(team *model.Team, event string) { message := model.NewWebSocketEvent(event, "", "", "", nil) message.Add("team", sanitizedTeam.ToJson()) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) UpdateTeamMemberRoles(teamId string, userId string, newRoles string) (*model.TeamMember, *model.AppError) { @@ -173,10 +176,7 @@ func (a *App) UpdateTeamMemberRoles(teamId string, userId string, newRoles strin func (a *App) sendUpdatedMemberRoleEvent(userId string, member *model.TeamMember) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_MEMBERROLE_UPDATED, "", "", userId, nil) message.Add("member", member.ToJson()) - - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) AddUserToTeam(teamId string, userId string, userRequestorId string) (*model.Team, *model.AppError) { @@ -919,3 +919,88 @@ func (a *App) SanitizeTeams(session model.Session, teams []*model.Team) []*model return teams } + +func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) { + if len(*a.Config().FileSettings.DriverName) == 0 { + return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.filesettings_no_driver.app_error", nil, "", http.StatusNotImplemented) + } else { + path := "teams/" + team.Id + "/teamIcon.png" + if data, err := a.ReadFile(path); err != nil { + return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.read_file.app_error", nil, err.Error(), http.StatusNotFound) + } else { + return data, nil + } + } +} + +func (a *App) SetTeamIcon(teamId string, imageData *multipart.FileHeader) *model.AppError { + file, err := imageData.Open() + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, err.Error(), http.StatusBadRequest) + } + defer file.Close() + return a.SetTeamIconFromFile(teamId, file) +} + +func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.AppError { + + team, getTeamErr := a.GetTeam(teamId) + + if getTeamErr != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.get_team.app_error", nil, getTeamErr.Error(), http.StatusBadRequest) + } + + if len(*a.Config().FileSettings.DriverName) == 0 { + return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented) + } + + // Decode image config first to check dimensions before loading the whole thing into memory later on + config, _, err := image.DecodeConfig(file) + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode_config.app_error", nil, err.Error(), http.StatusBadRequest) + } else if config.Width*config.Height > model.MaxImageSize { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, err.Error(), http.StatusBadRequest) + } + + file.Seek(0, 0) + + // Decode image into Image object + img, _, err := image.Decode(file) + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, err.Error(), http.StatusBadRequest) + } + + file.Seek(0, 0) + + orientation, _ := getImageOrientation(file) + img = makeImageUpright(img, orientation) + + // Scale team icon + teamIconWidthAndHeight := 128 + img = imaging.Fill(img, teamIconWidthAndHeight, teamIconWidthAndHeight, imaging.Center, imaging.Lanczos) + + buf := new(bytes.Buffer) + err = png.Encode(buf, img) + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + path := "teams/" + teamId + "/teamIcon.png" + + if err := a.WriteFile(buf.Bytes(), path); err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError) + } + + curTime := model.GetMillis() + + if result := <-a.Srv.Store.Team().UpdateLastTeamIconUpdate(teamId, curTime); result.Err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.update.app_error", nil, result.Err.Error(), http.StatusBadRequest) + } + + // manually set time to avoid possible cluster inconsistencies + team.LastTeamIconUpdate = curTime + + a.sendTeamEvent(team, model.WEBSOCKET_EVENT_UPDATE_TEAM) + + return nil +} diff --git a/app/user.go b/app/user.go index f915f35cb..7a6dc0b49 100644 --- a/app/user.go +++ b/app/user.go @@ -34,7 +34,6 @@ const ( TOKEN_TYPE_PASSWORD_RECOVERY = "password_recovery" TOKEN_TYPE_VERIFY_EMAIL = "verify_email" PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour - VERIFY_EMAIL_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour IMAGE_PROFILE_PIXEL_DIMENSION = 128 ) @@ -202,9 +201,7 @@ func (a *App) CreateUser(user *model.User) (*model.User, *model.AppError) { // This message goes to everyone, so the teamId, channelId and userId are irrelevant message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) message.Add("user_id", ruser.Id) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return ruser, nil } @@ -508,6 +505,14 @@ func (a *App) GetUsersInChannel(channelId string, offset int, limit int) ([]*mod } } +func (a *App) GetUsersInChannelByStatus(channelId string, offset int, limit int) ([]*model.User, *model.AppError) { + if result := <-a.Srv.Store.User().GetProfilesInChannelByStatus(channelId, offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.User), nil + } +} + func (a *App) GetUsersInChannelMap(channelId string, offset int, limit int, asAdmin bool) (map[string]*model.User, *model.AppError) { users, err := a.GetUsersInChannel(channelId, offset, limit) if err != nil { @@ -533,6 +538,15 @@ func (a *App) GetUsersInChannelPage(channelId string, page int, perPage int, asA return a.sanitizeProfiles(users, asAdmin), nil } +func (a *App) GetUsersInChannelPageByStatus(channelId string, page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { + users, err := a.GetUsersInChannelByStatus(channelId, page*perPage, perPage) + if err != nil { + return nil, err + } + + return a.sanitizeProfiles(users, asAdmin), nil +} + func (a *App) GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) ([]*model.User, *model.AppError) { if result := <-a.Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit); result.Err != nil { return nil, result.Err @@ -832,7 +846,6 @@ func (a *App) SetProfileImageFromFile(userId string, file multipart.File) *model message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) message.Add("user", user) - a.Publish(message) } @@ -901,10 +914,6 @@ func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.A } } - if extra := <-a.Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil { - return nil, extra.Err - } - ruser := result.Data.([2]*model.User)[0] options := a.Config().GetSanitizeOptions() options["passwordupdate"] = false @@ -1002,9 +1011,7 @@ func (a *App) sendUpdatedUserEvent(user model.User, asAdmin bool) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) message.Add("user", user) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) { diff --git a/app/user_test.go b/app/user_test.go index 38ff286b3..94052da61 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -299,3 +299,132 @@ func createGitlabUser(t *testing.T, a *App, email string, username string) (*mod return user, gitlabUserObj } + +func TestGetUsersByStatus(t *testing.T) { + th := Setup() + defer th.TearDown() + + team := th.CreateTeam() + channel, err := th.App.CreateChannel(&model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: "name_" + model.NewId(), + Type: model.CHANNEL_OPEN, + TeamId: team.Id, + CreatorId: model.NewId(), + }, false) + if err != nil { + t.Fatalf("failed to create channel: %v", err) + } + + createUserWithStatus := func(username string, status string) *model.User { + id := model.NewId() + + user, err := th.App.CreateUser(&model.User{ + Email: "success+" + id + "@simulator.amazonses.com", + Username: "un_" + username + "_" + id, + Nickname: "nn_" + id, + Password: "Password1", + }) + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + + th.LinkUserToTeam(user, team) + th.AddUserToChannel(user, channel) + + th.App.SaveAndBroadcastStatus(&model.Status{ + UserId: user.Id, + Status: status, + Manual: true, + }) + + return user + } + + // Creating these out of order in case that affects results + awayUser1 := createUserWithStatus("away1", model.STATUS_AWAY) + awayUser2 := createUserWithStatus("away2", model.STATUS_AWAY) + dndUser1 := createUserWithStatus("dnd1", model.STATUS_DND) + dndUser2 := createUserWithStatus("dnd2", model.STATUS_DND) + offlineUser1 := createUserWithStatus("offline1", model.STATUS_OFFLINE) + offlineUser2 := createUserWithStatus("offline2", model.STATUS_OFFLINE) + onlineUser1 := createUserWithStatus("online1", model.STATUS_ONLINE) + onlineUser2 := createUserWithStatus("online2", model.STATUS_ONLINE) + + t.Run("sorting by status then alphabetical", func(t *testing.T) { + usersByStatus, err := th.App.GetUsersInChannelPageByStatus(channel.Id, 0, 8, true) + if err != nil { + t.Fatal(err) + } + + expectedUsersByStatus := []*model.User{ + onlineUser1, + onlineUser2, + awayUser1, + awayUser2, + dndUser1, + dndUser2, + offlineUser1, + offlineUser2, + } + + if len(usersByStatus) != len(expectedUsersByStatus) { + t.Fatalf("received only %v users, expected %v", len(usersByStatus), len(expectedUsersByStatus)) + } + + for i := range usersByStatus { + if usersByStatus[i].Id != expectedUsersByStatus[i].Id { + t.Fatalf("received user %v at index %v, expected %v", usersByStatus[i].Username, i, expectedUsersByStatus[i].Username) + } + } + }) + + t.Run("paging", func(t *testing.T) { + usersByStatus, err := th.App.GetUsersInChannelPageByStatus(channel.Id, 0, 3, true) + if err != nil { + t.Fatal(err) + } + + if len(usersByStatus) != 3 { + t.Fatal("received too many users") + } + + if usersByStatus[0].Id != onlineUser1.Id && usersByStatus[1].Id != onlineUser2.Id { + t.Fatal("expected to receive online users first") + } + + if usersByStatus[2].Id != awayUser1.Id { + t.Fatal("expected to receive away users second") + } + + usersByStatus, err = th.App.GetUsersInChannelPageByStatus(channel.Id, 1, 3, true) + if err != nil { + t.Fatal(err) + } + + if usersByStatus[0].Id != awayUser2.Id { + t.Fatal("expected to receive away users second") + } + + if usersByStatus[1].Id != dndUser1.Id && usersByStatus[2].Id != dndUser2.Id { + t.Fatal("expected to receive dnd users third") + } + + usersByStatus, err = th.App.GetUsersInChannelPageByStatus(channel.Id, 1, 4, true) + if err != nil { + t.Fatal(err) + } + + if len(usersByStatus) != 4 { + t.Fatal("received too many users") + } + + if usersByStatus[0].Id != dndUser1.Id && usersByStatus[1].Id != dndUser2.Id { + t.Fatal("expected to receive dnd users third") + } + + if usersByStatus[2].Id != offlineUser1.Id && usersByStatus[3].Id != offlineUser2.Id { + t.Fatal("expected to receive offline users last") + } + }) +} diff --git a/app/webhook.go b/app/webhook.go index f3777ab48..abfc388b5 100644 --- a/app/webhook.go +++ b/app/webhook.go @@ -225,7 +225,7 @@ func SplitWebhookPost(post *model.Post) ([]*model.Post, *model.AppError) { func (a *App) CreateWebhookPost(userId string, channel *model.Channel, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) { // parse links into Markdown format - linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + linkWithTextRegex := regexp.MustCompile(`<([^\n<\|>]+)\|([^\n>]+)>`) text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") post := &model.Post{UserId: userId, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId} diff --git a/app/webhook_test.go b/app/webhook_test.go index 850e74efc..4d2bc58fa 100644 --- a/app/webhook_test.go +++ b/app/webhook_test.go @@ -317,6 +317,64 @@ func TestCreateWebhookPost(t *testing.T) { if err == nil { t.Fatal("should have failed - bad post type") } + + expectedText := "`<>|<>|`" + post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ + "attachments": []*model.SlackAttachment{ + { + Text: "text", + }, + }, + "webhook_display_name": hook.DisplayName, + }, model.POST_SLACK_ATTACHMENT, "") + if err != nil { + t.Fatal(err.Error()) + } + assert.Equal(t, expectedText, post.Message) + + expectedText = "< | \n|\n>" + post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ + "attachments": []*model.SlackAttachment{ + { + Text: "text", + }, + }, + "webhook_display_name": hook.DisplayName, + }, model.POST_SLACK_ATTACHMENT, "") + if err != nil { + t.Fatal(err.Error()) + } + assert.Equal(t, expectedText, post.Message) + + expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a +Author: foo <foo@example.org> +Date: Thu Mar 1 19:46:54 2018 +0300 + + commit message 2 + + test | 1 + + 1 file changed, 1 insertion(+) + +commit 5df78b7139b543997838071cd912e375d8bd69b2 +Author: foo <foo@example.org> +Date: Thu Mar 1 19:46:48 2018 +0300 + + commit message 1 + + test | 3 +++ + 1 file changed, 3 insertions(+)` + post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ + "attachments": []*model.SlackAttachment{ + { + Text: "text", + }, + }, + "webhook_display_name": hook.DisplayName, + }, model.POST_SLACK_ATTACHMENT, "") + if err != nil { + t.Fatal(err.Error()) + } + assert.Equal(t, expectedText, post.Message) } func TestSplitWebhookPost(t *testing.T) { diff --git a/build/Jenkinsfile b/build/Jenkinsfile index ebef7ca3d..7eb2fda88 100644 --- a/build/Jenkinsfile +++ b/build/Jenkinsfile @@ -31,7 +31,7 @@ podTemplate(label: 'jenkins-slave', ), containerTemplate( name: 'mattermost-inbucket', - image: 'jhillyerd/inbucket:latest', + image: 'jhillyerd/inbucket:release-1.2.0', resourceRequestCpu: '250m', resourceLimitCpu: '250m', resourceRequestMemory: '256Mi', diff --git a/build/release.mk b/build/release.mk index 8bd4f9afd..568616ec3 100644 --- a/build/release.mk +++ b/build/release.mk @@ -3,15 +3,27 @@ dist: | check-style test package build-linux: @echo Build Linux amd64 - env GOOS=linux GOARCH=amd64 $(GO) install $(GOFLAGS) $(GO_LINKER_FLAGS) ./cmd/platform +ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64") + env GOOS=linux GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./ +else + env GOOS=linux GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/linux_amd64/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./ +endif build-osx: @echo Build OSX amd64 - env GOOS=darwin GOARCH=amd64 $(GO) install $(GOFLAGS) $(GO_LINKER_FLAGS) ./cmd/platform +ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64") + env GOOS=darwin GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./ +else + env GOOS=darwin GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/darwin_amd64/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./ +endif build-windows: @echo Build Windows amd64 - env GOOS=windows GOARCH=amd64 $(GO) install $(GOFLAGS) $(GO_LINKER_FLAGS) ./cmd/platform +ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64") + env GOOS=windows GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/platform.exe $(GOFLAGS) $(GO_LINKER_FLAGS) ./ +else + env GOOS=windows GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/windows_amd64/platform.exe $(GOFLAGS) $(GO_LINKER_FLAGS) ./ +endif build: build-linux build-windows build-osx diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 000000000..5a1a25bd9 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +type Command = cobra.Command + +func Run(args []string) error { + RootCmd.SetArgs(args) + return RootCmd.Execute() +} + +var RootCmd = &cobra.Command{ + Use: "platform", + Short: "Open source, self-hosted Slack-alternative", + Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`, +} + +func init() { + RootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.") + RootCmd.PersistentFlags().Bool("disableconfigwatch", false, "When set config.json will not be loaded from disk when the file is changed.") +} diff --git a/cmd/platform/platform_test.go b/cmd/cmdtestlib.go index 792cabe38..db97b1a41 100644 --- a/cmd/platform/platform_test.go +++ b/cmd/cmdtestlib.go @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package cmd import ( "flag" @@ -30,7 +30,7 @@ func execArgs(t *testing.T, args []string) []string { return append(append(ret, "--", "--disableconfigwatch"), args...) } -func checkCommand(t *testing.T, args ...string) string { +func CheckCommand(t *testing.T, args ...string) string { path, err := os.Executable() require.NoError(t, err) output, err := exec.Command(path, execArgs(t, args)...).CombinedOutput() @@ -38,16 +38,8 @@ func checkCommand(t *testing.T, args ...string) string { return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(string(output)), "PASS")) } -func runCommand(t *testing.T, args ...string) error { +func RunCommand(t *testing.T, args ...string) error { path, err := os.Executable() require.NoError(t, err) return exec.Command(path, execArgs(t, args)...).Run() } - -func TestExecCommand(t *testing.T) { - if filter := flag.Lookup("test.run").Value.String(); filter != "ExecCommand" { - t.Skip("use -run ExecCommand to execute a command via the test executable") - } - rootCmd.SetArgs(flag.Args()) - require.NoError(t, rootCmd.Execute()) -} diff --git a/cmd/platform/channel.go b/cmd/commands/channel.go index 5d86ad9da..597a22450 100644 --- a/cmd/platform/channel.go +++ b/cmd/commands/channel.go @@ -1,22 +1,24 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "errors" "fmt" "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/spf13/cobra" ) -var channelCmd = &cobra.Command{ +var ChannelCmd = &cobra.Command{ Use: "channel", Short: "Management of channels", } -var channelCreateCmd = &cobra.Command{ +var ChannelCreateCmd = &cobra.Command{ Use: "create", Short: "Create a channel", Long: `Create a channel.`, @@ -25,7 +27,7 @@ var channelCreateCmd = &cobra.Command{ RunE: createChannelCmdF, } -var removeChannelUsersCmd = &cobra.Command{ +var RemoveChannelUsersCmd = &cobra.Command{ Use: "remove [channel] [users]", Short: "Remove users from channel", Long: "Remove some users from channel", @@ -33,7 +35,7 @@ var removeChannelUsersCmd = &cobra.Command{ RunE: removeChannelUsersCmdF, } -var addChannelUsersCmd = &cobra.Command{ +var AddChannelUsersCmd = &cobra.Command{ Use: "add [channel] [users]", Short: "Add users to channel", Long: "Add some users to channel", @@ -41,7 +43,7 @@ var addChannelUsersCmd = &cobra.Command{ RunE: addChannelUsersCmdF, } -var archiveChannelsCmd = &cobra.Command{ +var ArchiveChannelsCmd = &cobra.Command{ Use: "archive [channels]", Short: "Archive channels", Long: `Archive some channels. @@ -51,7 +53,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe RunE: archiveChannelsCmdF, } -var deleteChannelsCmd = &cobra.Command{ +var DeleteChannelsCmd = &cobra.Command{ Use: "delete [channels]", Short: "Delete channels", Long: `Permanently delete some channels. @@ -61,7 +63,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe RunE: deleteChannelsCmdF, } -var listChannelsCmd = &cobra.Command{ +var ListChannelsCmd = &cobra.Command{ Use: "list [teams]", Short: "List all channels on specified teams.", Long: `List all channels on specified teams. @@ -70,7 +72,7 @@ Archived channels are appended with ' (archived)'.`, RunE: listChannelsCmdF, } -var moveChannelsCmd = &cobra.Command{ +var MoveChannelsCmd = &cobra.Command{ Use: "move [team] [channels]", Short: "Moves channels to the specified team", Long: `Moves the provided channels to the specified team. @@ -80,7 +82,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe RunE: moveChannelsCmdF, } -var restoreChannelsCmd = &cobra.Command{ +var RestoreChannelsCmd = &cobra.Command{ Use: "restore [channels]", Short: "Restore some channels", Long: `Restore a previously deleted channel @@ -89,7 +91,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe RunE: restoreChannelsCmdF, } -var modifyChannelCmd = &cobra.Command{ +var ModifyChannelCmd = &cobra.Command{ Use: "modify [channel]", Short: "Modify a channel's public/private type", Long: `Change the public/private type of a channel. @@ -99,55 +101,57 @@ Channel can be specified by [team]:[channel]. ie. myteam:mychannel or by channel } func init() { - channelCreateCmd.Flags().String("name", "", "Channel Name") - channelCreateCmd.Flags().String("display_name", "", "Channel Display Name") - channelCreateCmd.Flags().String("team", "", "Team name or ID") - channelCreateCmd.Flags().String("header", "", "Channel header") - channelCreateCmd.Flags().String("purpose", "", "Channel purpose") - channelCreateCmd.Flags().Bool("private", false, "Create a private channel.") - - moveChannelsCmd.Flags().String("username", "", "Required. Username who is moving the channel.") - - deleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channels.") - - modifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel") - modifyChannelCmd.Flags().Bool("public", false, "Convert the channel to a public channel") - modifyChannelCmd.Flags().String("username", "", "Required. Username who changes the channel privacy.") - - channelCmd.AddCommand( - channelCreateCmd, - removeChannelUsersCmd, - addChannelUsersCmd, - archiveChannelsCmd, - deleteChannelsCmd, - listChannelsCmd, - moveChannelsCmd, - restoreChannelsCmd, - modifyChannelCmd, + ChannelCreateCmd.Flags().String("name", "", "Channel Name") + ChannelCreateCmd.Flags().String("display_name", "", "Channel Display Name") + ChannelCreateCmd.Flags().String("team", "", "Team name or ID") + ChannelCreateCmd.Flags().String("header", "", "Channel header") + ChannelCreateCmd.Flags().String("purpose", "", "Channel purpose") + ChannelCreateCmd.Flags().Bool("private", false, "Create a private channel.") + + MoveChannelsCmd.Flags().String("username", "", "Required. Username who is moving the channel.") + + DeleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channels.") + + ModifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel") + ModifyChannelCmd.Flags().Bool("public", false, "Convert the channel to a public channel") + ModifyChannelCmd.Flags().String("username", "", "Required. Username who changes the channel privacy.") + + ChannelCmd.AddCommand( + ChannelCreateCmd, + RemoveChannelUsersCmd, + AddChannelUsersCmd, + ArchiveChannelsCmd, + DeleteChannelsCmd, + ListChannelsCmd, + MoveChannelsCmd, + RestoreChannelsCmd, + ModifyChannelCmd, ) + + cmd.RootCmd.AddCommand(ChannelCmd) } -func createChannelCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func createChannelCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } - name, errn := cmd.Flags().GetString("name") + name, errn := command.Flags().GetString("name") if errn != nil || name == "" { return errors.New("Name is required") } - displayname, errdn := cmd.Flags().GetString("display_name") + displayname, errdn := command.Flags().GetString("display_name") if errdn != nil || displayname == "" { return errors.New("Display Name is required") } - teamArg, errteam := cmd.Flags().GetString("team") + teamArg, errteam := command.Flags().GetString("team") if errteam != nil || teamArg == "" { return errors.New("Team is required") } - header, _ := cmd.Flags().GetString("header") - purpose, _ := cmd.Flags().GetString("purpose") - useprivate, _ := cmd.Flags().GetBool("private") + header, _ := command.Flags().GetString("header") + purpose, _ := command.Flags().GetString("purpose") + useprivate, _ := command.Flags().GetBool("private") channelType := model.CHANNEL_OPEN if useprivate { @@ -176,8 +180,8 @@ func createChannelCmdF(cmd *cobra.Command, args []string) error { return nil } -func removeChannelUsersCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func removeChannelUsersCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -201,16 +205,16 @@ func removeChannelUsersCmdF(cmd *cobra.Command, args []string) error { func removeUserFromChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) { if user == nil { - CommandPrintErrorln("Can't find user '" + userArg + "'") + cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") return } if err := a.RemoveUserFromChannel(user.Id, "", channel); err != nil { - CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) + cmd.CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) } } -func addChannelUsersCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func addChannelUsersCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -234,16 +238,16 @@ func addChannelUsersCmdF(cmd *cobra.Command, args []string) error { func addUserToChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) { if user == nil { - CommandPrintErrorln("Can't find user '" + userArg + "'") + cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") return } if _, err := a.AddUserToChannel(user, channel); err != nil { - CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) + cmd.CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) } } -func archiveChannelsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func archiveChannelsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -255,19 +259,19 @@ func archiveChannelsCmdF(cmd *cobra.Command, args []string) error { channels := getChannelsFromChannelArgs(a, args) for i, channel := range channels { if channel == nil { - CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") continue } if result := <-a.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil { - CommandPrintErrorln("Unable to archive channel '" + channel.Name + "' error: " + result.Err.Error()) + cmd.CommandPrintErrorln("Unable to archive channel '" + channel.Name + "' error: " + result.Err.Error()) } } return nil } -func deleteChannelsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func deleteChannelsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -276,10 +280,10 @@ func deleteChannelsCmdF(cmd *cobra.Command, args []string) error { return errors.New("Enter at least one channel to delete.") } - confirmFlag, _ := cmd.Flags().GetBool("confirm") + confirmFlag, _ := command.Flags().GetBool("confirm") if !confirmFlag { var confirm string - CommandPrettyPrintln("Are you sure you want to delete the channels specified? All data will be permanently deleted? (YES/NO): ") + cmd.CommandPrettyPrintln("Are you sure you want to delete the channels specified? All data will be permanently deleted? (YES/NO): ") fmt.Scanln(&confirm) if confirm != "YES" { return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") @@ -289,13 +293,13 @@ func deleteChannelsCmdF(cmd *cobra.Command, args []string) error { channels := getChannelsFromChannelArgs(a, args) for i, channel := range channels { if channel == nil { - CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") continue } if err := deleteChannel(a, channel); err != nil { - CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + err.Error()) + cmd.CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + err.Error()) } else { - CommandPrettyPrintln("Deleted channel '" + channel.Name + "'") + cmd.CommandPrettyPrintln("Deleted channel '" + channel.Name + "'") } } @@ -306,8 +310,8 @@ func deleteChannel(a *app.App, channel *model.Channel) *model.AppError { return a.PermanentDeleteChannel(channel) } -func moveChannelsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func moveChannelsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -321,7 +325,7 @@ func moveChannelsCmdF(cmd *cobra.Command, args []string) error { return errors.New("Unable to find destination team '" + args[0] + "'") } - username, erru := cmd.Flags().GetString("username") + username, erru := command.Flags().GetString("username") if erru != nil || username == "" { return errors.New("Username is required") } @@ -330,14 +334,14 @@ func moveChannelsCmdF(cmd *cobra.Command, args []string) error { channels := getChannelsFromChannelArgs(a, args[1:]) for i, channel := range channels { if channel == nil { - CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") continue } originTeamID := channel.TeamId if err := moveChannel(a, team, channel, user); err != nil { - CommandPrintErrorln("Unable to move channel '" + channel.Name + "' error: " + err.Error()) + cmd.CommandPrintErrorln("Unable to move channel '" + channel.Name + "' error: " + err.Error()) } else { - CommandPrettyPrintln("Moved channel '" + channel.Name + "' to " + team.Name + "(" + team.Id + ") from " + originTeamID + ".") + cmd.CommandPrettyPrintln("Moved channel '" + channel.Name + "' to " + team.Name + "(" + team.Id + ") from " + originTeamID + ".") } } @@ -358,7 +362,7 @@ func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *mod if webhook.ChannelId == channel.Id { webhook.TeamId = team.Id if result := <-a.Srv.Store.Webhook().UpdateIncoming(webhook); result.Err != nil { - CommandPrintErrorln("Failed to move incoming webhook '" + webhook.Id + "' to new team.") + cmd.CommandPrintErrorln("Failed to move incoming webhook '" + webhook.Id + "' to new team.") } } } @@ -371,7 +375,7 @@ func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *mod if webhook.ChannelId == channel.Id { webhook.TeamId = team.Id if result := <-a.Srv.Store.Webhook().UpdateOutgoing(webhook); result.Err != nil { - CommandPrintErrorln("Failed to move outgoing webhook '" + webhook.Id + "' to new team.") + cmd.CommandPrintErrorln("Failed to move outgoing webhook '" + webhook.Id + "' to new team.") } } } @@ -380,8 +384,8 @@ func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *mod return nil } -func listChannelsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func listChannelsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -393,19 +397,19 @@ func listChannelsCmdF(cmd *cobra.Command, args []string) error { teams := getTeamsFromTeamArgs(a, args) for i, team := range teams { if team == nil { - CommandPrintErrorln("Unable to find team '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find team '" + args[i] + "'") continue } if result := <-a.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { - CommandPrintErrorln("Unable to list channels for '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to list channels for '" + args[i] + "'") } else { channels := result.Data.([]*model.Channel) for _, channel := range channels { if channel.DeleteAt > 0 { - CommandPrettyPrintln(channel.Name + " (archived)") + cmd.CommandPrettyPrintln(channel.Name + " (archived)") } else { - CommandPrettyPrintln(channel.Name) + cmd.CommandPrettyPrintln(channel.Name) } } } @@ -414,8 +418,8 @@ func listChannelsCmdF(cmd *cobra.Command, args []string) error { return nil } -func restoreChannelsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func restoreChannelsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -427,19 +431,19 @@ func restoreChannelsCmdF(cmd *cobra.Command, args []string) error { channels := getChannelsFromChannelArgs(a, args) for i, channel := range channels { if channel == nil { - CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") continue } if result := <-a.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { - CommandPrintErrorln("Unable to restore channel '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to restore channel '" + args[i] + "'") } } return nil } -func modifyChannelCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func modifyChannelCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -448,13 +452,13 @@ func modifyChannelCmdF(cmd *cobra.Command, args []string) error { return errors.New("Enter at one channel to modify.") } - username, erru := cmd.Flags().GetString("username") + username, erru := command.Flags().GetString("username") if erru != nil || username == "" { return errors.New("Username is required") } - public, _ := cmd.Flags().GetBool("public") - private, _ := cmd.Flags().GetBool("private") + public, _ := command.Flags().GetBool("public") + private, _ := command.Flags().GetBool("private") if public == private { return errors.New("You must specify only one of --public or --private") diff --git a/cmd/platform/channel_test.go b/cmd/commands/channel_test.go index cf8603cf3..bd19b020a 100644 --- a/cmd/platform/channel_test.go +++ b/cmd/commands/channel_test.go @@ -1,13 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "strings" "testing" "github.com/mattermost/mattermost-server/api" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/require" ) @@ -18,13 +19,13 @@ func TestJoinChannel(t *testing.T) { channel := th.CreateChannel(th.BasicClient, th.BasicTeam) - checkCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) // Joining twice should succeed - checkCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) // should fail because channel does not exist - require.Error(t, runCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name+"asdf", th.BasicUser2.Email)) + require.Error(t, cmd.RunCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name+"asdf", th.BasicUser2.Email)) } func TestRemoveChannel(t *testing.T) { @@ -33,15 +34,15 @@ func TestRemoveChannel(t *testing.T) { channel := th.CreateChannel(th.BasicClient, th.BasicTeam) - checkCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) // should fail because channel does not exist - require.Error(t, runCommand(t, "channel", "remove", th.BasicTeam.Name+":doesnotexist", th.BasicUser2.Email)) + require.Error(t, cmd.RunCommand(t, "channel", "remove", th.BasicTeam.Name+":doesnotexist", th.BasicUser2.Email)) - checkCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + cmd.CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) // Leaving twice should succeed - checkCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + cmd.CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) } func TestMoveChannel(t *testing.T) { @@ -63,12 +64,12 @@ func TestMoveChannel(t *testing.T) { origin := team1.Name + ":" + channel.Name dest := team2.Name - checkCommand(t, "channel", "add", origin, adminEmail) + cmd.CheckCommand(t, "channel", "add", origin, adminEmail) // should fail with nill because errors are logged instead of returned when a channel does not exist - require.Nil(t, runCommand(t, "channel", "move", dest, team1.Name+":doesnotexist", "--username", adminUsername)) + require.Nil(t, cmd.RunCommand(t, "channel", "move", dest, team1.Name+":doesnotexist", "--username", adminUsername)) - checkCommand(t, "channel", "move", dest, origin, "--username", adminUsername) + cmd.CheckCommand(t, "channel", "move", dest, origin, "--username", adminUsername) } func TestListChannels(t *testing.T) { @@ -78,7 +79,7 @@ func TestListChannels(t *testing.T) { channel := th.CreateChannel(th.BasicClient, th.BasicTeam) th.BasicClient.Must(th.BasicClient.DeleteChannel(channel.Id)) - output := checkCommand(t, "channel", "list", th.BasicTeam.Name) + output := cmd.CheckCommand(t, "channel", "list", th.BasicTeam.Name) if !strings.Contains(string(output), "town-square") { t.Fatal("should have channels") @@ -96,10 +97,10 @@ func TestRestoreChannel(t *testing.T) { channel := th.CreateChannel(th.BasicClient, th.BasicTeam) th.BasicClient.Must(th.BasicClient.DeleteChannel(channel.Id)) - checkCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) + cmd.CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) // restoring twice should succeed - checkCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) + cmd.CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) } func TestCreateChannel(t *testing.T) { @@ -109,8 +110,8 @@ func TestCreateChannel(t *testing.T) { id := model.NewId() name := "name" + id - checkCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--name", name) + cmd.CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--name", name) name = name + "-private" - checkCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--private", "--name", name) + cmd.CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--private", "--name", name) } diff --git a/cmd/platform/channelargs.go b/cmd/commands/channelargs.go index c12a9cc9a..680fed34b 100644 --- a/cmd/platform/channelargs.go +++ b/cmd/commands/channelargs.go @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "fmt" diff --git a/cmd/platform/command.go b/cmd/commands/command.go index bbc5b47da..0202714b6 100644 --- a/cmd/platform/command.go +++ b/cmd/commands/command.go @@ -1,20 +1,22 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "errors" "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/spf13/cobra" ) -var commandCmd = &cobra.Command{ +var CommandCmd = &cobra.Command{ Use: "command", Short: "Management of slash commands", } -var commandMoveCmd = &cobra.Command{ +var CommandMoveCmd = &cobra.Command{ Use: "move", Short: "Move a slash command to a different team", Long: `Move a slash command to a different team. Commands can be specified by [team]:[command-trigger-word]. ie. myteam:trigger or by command ID.`, @@ -23,13 +25,14 @@ var commandMoveCmd = &cobra.Command{ } func init() { - commandCmd.AddCommand( - commandMoveCmd, + CommandCmd.AddCommand( + CommandMoveCmd, ) + cmd.RootCmd.AddCommand(CommandCmd) } -func moveCommandCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func moveCommandCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -44,16 +47,16 @@ func moveCommandCmdF(cmd *cobra.Command, args []string) error { } commands := getCommandsFromCommandArgs(a, args[1:]) - CommandPrintErrorln(commands) + cmd.CommandPrintErrorln(commands) for i, command := range commands { if command == nil { - CommandPrintErrorln("Unable to find command '" + args[i+1] + "'") + cmd.CommandPrintErrorln("Unable to find command '" + args[i+1] + "'") continue } if err := moveCommand(a, team, command); err != nil { - CommandPrintErrorln("Unable to move command '" + command.Trigger + "' error: " + err.Error()) + cmd.CommandPrintErrorln("Unable to move command '" + command.Trigger + "' error: " + err.Error()) } else { - CommandPrettyPrintln("Moved command '" + command.Trigger + "'") + cmd.CommandPrettyPrintln("Moved command '" + command.Trigger + "'") } } diff --git a/cmd/platform/commandargs.go b/cmd/commands/commandargs.go index 96e756815..702f01c9a 100644 --- a/cmd/platform/commandargs.go +++ b/cmd/commands/commandargs.go @@ -1,6 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "fmt" diff --git a/cmd/platform/config.go b/cmd/commands/config.go index cd4356529..ef3b0f75e 100644 --- a/cmd/platform/config.go +++ b/cmd/commands/config.go @@ -1,23 +1,25 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "encoding/json" "errors" "os" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" "github.com/spf13/cobra" ) -var configCmd = &cobra.Command{ +var ConfigCmd = &cobra.Command{ Use: "config", Short: "Configuration", } -var validateConfigCmd = &cobra.Command{ +var ValidateConfigCmd = &cobra.Command{ Use: "validate", Short: "Validate config file", Long: "If the config file is valid, this command will output a success message and have a zero exit code. If it is invalid, this command will output an error and have a non-zero exit code.", @@ -25,15 +27,16 @@ var validateConfigCmd = &cobra.Command{ } func init() { - configCmd.AddCommand( - validateConfigCmd, + ConfigCmd.AddCommand( + ValidateConfigCmd, ) + cmd.RootCmd.AddCommand(ConfigCmd) } -func configValidateCmdF(cmd *cobra.Command, args []string) error { +func configValidateCmdF(command *cobra.Command, args []string) error { utils.TranslationsPreInit() model.AppErrorInit(utils.T) - filePath, err := cmd.Flags().GetString("config") + filePath, err := command.Flags().GetString("config") if err != nil { return err } @@ -60,6 +63,6 @@ func configValidateCmdF(cmd *cobra.Command, args []string) error { return errors.New(utils.T(err.Id)) } - CommandPrettyPrintln("The document is valid") + cmd.CommandPrettyPrintln("The document is valid") return nil } diff --git a/cmd/platform/mattermost_test.go b/cmd/commands/config_flag_test.go index 7246d620f..8d284ab73 100644 --- a/cmd/platform/mattermost_test.go +++ b/cmd/commands/config_flag_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "io/ioutil" @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/utils" ) @@ -34,8 +35,8 @@ func TestConfigFlag(t *testing.T) { defer os.Chdir(prevDir) os.Chdir(dir) - require.Error(t, runCommand(t, "version")) - checkCommand(t, "--config", "foo.json", "version") - checkCommand(t, "--config", "./foo.json", "version") - checkCommand(t, "--config", configPath, "version") + require.Error(t, cmd.RunCommand(t, "version")) + cmd.CheckCommand(t, "--config", "foo.json", "version") + cmd.CheckCommand(t, "--config", "./foo.json", "version") + cmd.CheckCommand(t, "--config", configPath, "version") } diff --git a/cmd/platform/config_test.go b/cmd/commands/config_test.go index f1c09c6f5..54ddfcb61 100644 --- a/cmd/platform/config_test.go +++ b/cmd/commands/config_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "io/ioutil" @@ -9,6 +9,7 @@ import ( "path/filepath" "testing" + "github.com/mattermost/mattermost-server/cmd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,6 +26,6 @@ func TestConfigValidate(t *testing.T) { config.SetDefaults() require.NoError(t, ioutil.WriteFile(path, []byte(config.ToJson()), 0600)) - assert.Error(t, runCommand(t, "--config", "foo.json", "config", "validate")) - assert.NoError(t, runCommand(t, "--config", path, "config", "validate")) + assert.Error(t, cmd.RunCommand(t, "--config", "foo.json", "config", "validate")) + assert.NoError(t, cmd.RunCommand(t, "--config", path, "config", "validate")) } diff --git a/cmd/commands/exec_command_test.go b/cmd/commands/exec_command_test.go new file mode 100644 index 000000000..79e65fe83 --- /dev/null +++ b/cmd/commands/exec_command_test.go @@ -0,0 +1,21 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/cmd" +) + +func TestExecCommand(t *testing.T) { + if filter := flag.Lookup("test.run").Value.String(); filter != "ExecCommand" { + t.Skip("use -run ExecCommand to execute a command via the test executable") + } + cmd.RootCmd.SetArgs(flag.Args()) + require.NoError(t, cmd.RootCmd.Execute()) +} diff --git a/cmd/platform/import.go b/cmd/commands/import.go index 44ada904f..4058d175a 100644 --- a/cmd/platform/import.go +++ b/cmd/commands/import.go @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "errors" @@ -8,15 +9,16 @@ import ( "fmt" + "github.com/mattermost/mattermost-server/cmd" "github.com/spf13/cobra" ) -var importCmd = &cobra.Command{ +var ImportCmd = &cobra.Command{ Use: "import", Short: "Import data.", } -var slackImportCmd = &cobra.Command{ +var SlackImportCmd = &cobra.Command{ Use: "slack [team] [file]", Short: "Import a team from Slack.", Long: "Import a team from a Slack export zip file.", @@ -24,7 +26,7 @@ var slackImportCmd = &cobra.Command{ RunE: slackImportCmdF, } -var bulkImportCmd = &cobra.Command{ +var BulkImportCmd = &cobra.Command{ Use: "bulk [file]", Short: "Import bulk data.", Long: "Import data from a Mattermost Bulk Import File.", @@ -33,18 +35,19 @@ var bulkImportCmd = &cobra.Command{ } func init() { - bulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.") - bulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.") - bulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.") + BulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.") + BulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.") + BulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.") - importCmd.AddCommand( - bulkImportCmd, - slackImportCmd, + ImportCmd.AddCommand( + BulkImportCmd, + SlackImportCmd, ) + cmd.RootCmd.AddCommand(ImportCmd) } -func slackImportCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func slackImportCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -69,32 +72,32 @@ func slackImportCmdF(cmd *cobra.Command, args []string) error { return err } - CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.") + cmd.CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.") a.SlackImport(fileReader, fileInfo.Size(), team.Id) - CommandPrettyPrintln("Finished Slack Import.") + cmd.CommandPrettyPrintln("Finished Slack Import.") return nil } -func bulkImportCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func bulkImportCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } - apply, err := cmd.Flags().GetBool("apply") + apply, err := command.Flags().GetBool("apply") if err != nil { return errors.New("Apply flag error") } - validate, err := cmd.Flags().GetBool("validate") + validate, err := command.Flags().GetBool("validate") if err != nil { return errors.New("Validate flag error") } - workers, err := cmd.Flags().GetInt("workers") + workers, err := command.Flags().GetInt("workers") if err != nil { return errors.New("Workers flag error") } @@ -110,28 +113,28 @@ func bulkImportCmdF(cmd *cobra.Command, args []string) error { defer fileReader.Close() if apply && validate { - CommandPrettyPrintln("Use only one of --apply or --validate.") + cmd.CommandPrettyPrintln("Use only one of --apply or --validate.") return nil } else if apply && !validate { - CommandPrettyPrintln("Running Bulk Import. This may take a long time.") + cmd.CommandPrettyPrintln("Running Bulk Import. This may take a long time.") } else { - CommandPrettyPrintln("Running Bulk Import Data Validation.") - CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **") - CommandPrettyPrintln("Use the --apply flag to perform the actual data import.") + cmd.CommandPrettyPrintln("Running Bulk Import Data Validation.") + cmd.CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **") + cmd.CommandPrettyPrintln("Use the --apply flag to perform the actual data import.") } - CommandPrettyPrintln("") + cmd.CommandPrettyPrintln("") if err, lineNumber := a.BulkImport(fileReader, !apply, workers); err != nil { - CommandPrettyPrintln(err.Error()) + cmd.CommandPrettyPrintln(err.Error()) if lineNumber != 0 { - CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber)) + cmd.CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber)) } } else { if apply { - CommandPrettyPrintln("Finished Bulk Import.") + cmd.CommandPrettyPrintln("Finished Bulk Import.") } else { - CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.") + cmd.CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.") } } diff --git a/cmd/platform/jobserver.go b/cmd/commands/jobserver.go index 044ee6b6a..b96984b41 100644 --- a/cmd/platform/jobserver.go +++ b/cmd/commands/jobserver.go @@ -1,6 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "os" @@ -8,27 +9,30 @@ import ( "syscall" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/mattermost-server/cmd" "github.com/spf13/cobra" ) -var jobserverCmd = &cobra.Command{ +var JobserverCmd = &cobra.Command{ Use: "jobserver", Short: "Start the Mattermost job server", Run: jobserverCmdF, } func init() { - jobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.") - jobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.") + JobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.") + JobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.") + + cmd.RootCmd.AddCommand(JobserverCmd) } -func jobserverCmdF(cmd *cobra.Command, args []string) { +func jobserverCmdF(command *cobra.Command, args []string) { // Options - noJobs, _ := cmd.Flags().GetBool("nojobs") - noSchedule, _ := cmd.Flags().GetBool("noschedule") + noJobs, _ := command.Flags().GetBool("nojobs") + noSchedule, _ := command.Flags().GetBool("noschedule") // Initialize - a, err := initDBCommandContext("config.json") + a, err := cmd.InitDBCommandContext("config.json") if err != nil { panic(err.Error()) } diff --git a/cmd/platform/ldap.go b/cmd/commands/ldap.go index 1bbcaa2f5..6938eae28 100644 --- a/cmd/platform/ldap.go +++ b/cmd/commands/ldap.go @@ -1,18 +1,20 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/spf13/cobra" ) -var ldapCmd = &cobra.Command{ +var LdapCmd = &cobra.Command{ Use: "ldap", Short: "LDAP related utilities", } -var ldapSyncCmd = &cobra.Command{ +var LdapSyncCmd = &cobra.Command{ Use: "sync", Short: "Synchronize now", Long: "Synchronize all LDAP users now.", @@ -21,13 +23,14 @@ var ldapSyncCmd = &cobra.Command{ } func init() { - ldapCmd.AddCommand( - ldapSyncCmd, + LdapCmd.AddCommand( + LdapSyncCmd, ) + cmd.RootCmd.AddCommand(LdapCmd) } -func ldapSyncCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func ldapSyncCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -35,9 +38,9 @@ func ldapSyncCmdF(cmd *cobra.Command, args []string) error { if ldapI := a.Ldap; ldapI != nil { job, err := ldapI.StartSynchronizeJob(true) if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED { - CommandPrintErrorln("ERROR: AD/LDAP Synchronization please check the server logs") + cmd.CommandPrintErrorln("ERROR: AD/LDAP Synchronization please check the server logs") } else { - CommandPrettyPrintln("SUCCESS: AD/LDAP Synchronization Complete") + cmd.CommandPrettyPrintln("SUCCESS: AD/LDAP Synchronization Complete") } } diff --git a/cmd/platform/license.go b/cmd/commands/license.go index 73efe9137..dce257a5d 100644 --- a/cmd/platform/license.go +++ b/cmd/commands/license.go @@ -1,20 +1,22 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "errors" "io/ioutil" + "github.com/mattermost/mattermost-server/cmd" "github.com/spf13/cobra" ) -var licenseCmd = &cobra.Command{ +var LicenseCmd = &cobra.Command{ Use: "license", Short: "Licensing commands", } -var uploadLicenseCmd = &cobra.Command{ +var UploadLicenseCmd = &cobra.Command{ Use: "upload [license]", Short: "Upload a license.", Long: "Upload a license. Replaces current license.", @@ -23,11 +25,12 @@ var uploadLicenseCmd = &cobra.Command{ } func init() { - licenseCmd.AddCommand(uploadLicenseCmd) + LicenseCmd.AddCommand(UploadLicenseCmd) + cmd.RootCmd.AddCommand(LicenseCmd) } -func uploadLicenseCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func uploadLicenseCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -45,7 +48,7 @@ func uploadLicenseCmdF(cmd *cobra.Command, args []string) error { return err } - CommandPrettyPrintln("Uploaded license file") + cmd.CommandPrettyPrintln("Uploaded license file") return nil } diff --git a/cmd/platform/message_export.go b/cmd/commands/message_export.go index fb1f4073b..7162d46c2 100644 --- a/cmd/platform/message_export.go +++ b/cmd/commands/message_export.go @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "errors" @@ -10,11 +10,12 @@ import ( "time" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/spf13/cobra" ) -var messageExportCmd = &cobra.Command{ +var MessageExportCmd = &cobra.Command{ Use: "export", Short: "Export data from Mattermost", Long: "Export data from Mattermost in a format suitable for import into a third-party application", @@ -23,13 +24,14 @@ var messageExportCmd = &cobra.Command{ } func init() { - messageExportCmd.Flags().String("format", "actiance", "The format to export data in") - messageExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.") - messageExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.") + MessageExportCmd.Flags().String("format", "actiance", "The format to export data in") + MessageExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.") + MessageExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.") + cmd.RootCmd.AddCommand(MessageExportCmd) } -func messageExportCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func messageExportCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -39,20 +41,20 @@ func messageExportCmdF(cmd *cobra.Command, args []string) error { } // for now, format is hard-coded to actiance. In time, we'll have to support other formats and inject them into job data - if format, err := cmd.Flags().GetString("format"); err != nil { + if format, err := command.Flags().GetString("format"); err != nil { return errors.New("format flag error") } else if format != "actiance" { return errors.New("unsupported export format") } - startTime, err := cmd.Flags().GetInt64("exportFrom") + startTime, err := command.Flags().GetInt64("exportFrom") if err != nil { return errors.New("exportFrom flag error") } else if startTime < 0 { return errors.New("exportFrom must be a positive integer") } - timeoutSeconds, err := cmd.Flags().GetInt("timeoutSeconds") + timeoutSeconds, err := command.Flags().GetInt("timeoutSeconds") if err != nil { return errors.New("timeoutSeconds error") } else if timeoutSeconds < 0 { @@ -69,9 +71,9 @@ func messageExportCmdF(cmd *cobra.Command, args []string) error { job, err := messageExportI.StartSynchronizeJob(ctx, startTime) if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED { - CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs") + cmd.CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs") } else { - CommandPrettyPrintln("SUCCESS: Message export job complete") + cmd.CommandPrettyPrintln("SUCCESS: Message export job complete") } } diff --git a/cmd/platform/message_export_test.go b/cmd/commands/message_export_test.go index 386aa4268..5170b77af 100644 --- a/cmd/platform/message_export_test.go +++ b/cmd/commands/message_export_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "io/ioutil" @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) @@ -24,7 +25,7 @@ func TestMessageExportNotEnabled(t *testing.T) { defer os.RemoveAll(filepath.Dir(configPath)) // should fail fast because the feature isn't enabled - require.Error(t, runCommand(t, "--config", configPath, "export")) + require.Error(t, cmd.RunCommand(t, "--config", configPath, "export")) } func TestMessageExportInvalidFormat(t *testing.T) { @@ -32,7 +33,7 @@ func TestMessageExportInvalidFormat(t *testing.T) { defer os.RemoveAll(filepath.Dir(configPath)) // should fail fast because format isn't supported - require.Error(t, runCommand(t, "--config", configPath, "--format", "not_actiance", "export")) + require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "not_actiance", "export")) } func TestMessageExportNegativeExportFrom(t *testing.T) { @@ -40,7 +41,7 @@ func TestMessageExportNegativeExportFrom(t *testing.T) { defer os.RemoveAll(filepath.Dir(configPath)) // should fail fast because export from must be a valid timestamp - require.Error(t, runCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "-1", "export")) + require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "-1", "export")) } func TestMessageExportNegativeTimeoutSeconds(t *testing.T) { @@ -48,7 +49,7 @@ func TestMessageExportNegativeTimeoutSeconds(t *testing.T) { defer os.RemoveAll(filepath.Dir(configPath)) // should fail fast because timeout seconds must be a positive int - require.Error(t, runCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "0", "--timeoutSeconds", "-1", "export")) + require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "0", "--timeoutSeconds", "-1", "export")) } func writeTempConfig(t *testing.T, isMessageExportEnabled bool) string { diff --git a/cmd/commands/reset.go b/cmd/commands/reset.go new file mode 100644 index 000000000..e479d0354 --- /dev/null +++ b/cmd/commands/reset.go @@ -0,0 +1,53 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "fmt" + + "github.com/mattermost/mattermost-server/cmd" + "github.com/spf13/cobra" +) + +var ResetCmd = &cobra.Command{ + Use: "reset", + Short: "Reset the database to initial state", + Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.", + RunE: resetCmdF, +} + +func init() { + ResetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.") + + cmd.RootCmd.AddCommand(ResetCmd) +} + +func resetCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) + if err != nil { + return err + } + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + cmd.CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + a.Srv.Store.DropAllTables() + cmd.CommandPrettyPrintln("Database sucessfully reset") + + return nil +} diff --git a/cmd/platform/roles.go b/cmd/commands/roles.go index e7a1c1a0e..bf7c39476 100644 --- a/cmd/platform/roles.go +++ b/cmd/commands/roles.go @@ -1,19 +1,21 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "errors" + "github.com/mattermost/mattermost-server/cmd" "github.com/spf13/cobra" ) -var rolesCmd = &cobra.Command{ +var RolesCmd = &cobra.Command{ Use: "roles", Short: "Management of user roles", } -var makeSystemAdminCmd = &cobra.Command{ +var MakeSystemAdminCmd = &cobra.Command{ Use: "system_admin [users]", Short: "Set a user as system admin", Long: "Make some users system admins", @@ -21,7 +23,7 @@ var makeSystemAdminCmd = &cobra.Command{ RunE: makeSystemAdminCmdF, } -var makeMemberCmd = &cobra.Command{ +var MakeMemberCmd = &cobra.Command{ Use: "member [users]", Short: "Remove system admin privileges", Long: "Remove system admin privileges from some users.", @@ -30,14 +32,15 @@ var makeMemberCmd = &cobra.Command{ } func init() { - rolesCmd.AddCommand( - makeSystemAdminCmd, - makeMemberCmd, + RolesCmd.AddCommand( + MakeSystemAdminCmd, + MakeMemberCmd, ) + cmd.RootCmd.AddCommand(RolesCmd) } -func makeSystemAdminCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func makeSystemAdminCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -60,8 +63,8 @@ func makeSystemAdminCmdF(cmd *cobra.Command, args []string) error { return nil } -func makeMemberCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func makeMemberCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } diff --git a/cmd/platform/roles_test.go b/cmd/commands/roles_test.go index 1a5ae5173..1e0a46a4e 100644 --- a/cmd/platform/roles_test.go +++ b/cmd/commands/roles_test.go @@ -1,12 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "testing" "github.com/mattermost/mattermost-server/api" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" ) @@ -14,7 +15,7 @@ func TestAssignRole(t *testing.T) { th := api.Setup().InitBasic() defer th.TearDown() - checkCommand(t, "roles", "system_admin", th.BasicUser.Email) + cmd.CheckCommand(t, "roles", "system_admin", th.BasicUser.Email) if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil { t.Fatal() diff --git a/cmd/platform/sampledata.go b/cmd/commands/sampledata.go index 98a041d8f..5377f1153 100644 --- a/cmd/platform/sampledata.go +++ b/cmd/commands/sampledata.go @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "encoding/json" @@ -16,15 +17,34 @@ import ( "github.com/icrowley/fake" "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/cmd" "github.com/spf13/cobra" ) -var sampleDataCmd = &cobra.Command{ +var SampleDataCmd = &cobra.Command{ Use: "sampledata", Short: "Generate sample data", RunE: sampleDataCmdF, } +func init() { + SampleDataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).") + SampleDataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.") + SampleDataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.") + SampleDataCmd.Flags().IntP("users", "u", 15, "The number of sample users.") + SampleDataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.") + SampleDataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.") + SampleDataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.") + SampleDataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.") + SampleDataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.") + SampleDataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.") + SampleDataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.") + SampleDataCmd.Flags().IntP("workers", "w", 2, "How many workers to run during the import.") + SampleDataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.") + SampleDataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of loading into the database.") + cmd.RootCmd.AddCommand(SampleDataCmd) +} + func sliceIncludes(vs []string, t string) bool { for _, v := range vs { if v == t { @@ -109,81 +129,64 @@ func randomMessage(users []string) string { return message } -func init() { - sampleDataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).") - sampleDataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.") - sampleDataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.") - sampleDataCmd.Flags().IntP("users", "u", 15, "The number of sample users.") - sampleDataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.") - sampleDataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.") - sampleDataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.") - sampleDataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.") - sampleDataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.") - sampleDataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.") - sampleDataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.") - sampleDataCmd.Flags().IntP("workers", "w", 2, "How many workers to run during the import.") - sampleDataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.") - sampleDataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of loading into the database.") -} - -func sampleDataCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func sampleDataCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } - seed, err := cmd.Flags().GetInt64("seed") + seed, err := command.Flags().GetInt64("seed") if err != nil { return errors.New("Invalid seed parameter") } - bulk, err := cmd.Flags().GetString("bulk") + bulk, err := command.Flags().GetString("bulk") if err != nil { return errors.New("Invalid bulk parameter") } - teams, err := cmd.Flags().GetInt("teams") + teams, err := command.Flags().GetInt("teams") if err != nil || teams < 0 { return errors.New("Invalid teams parameter") } - channelsPerTeam, err := cmd.Flags().GetInt("channels-per-team") + channelsPerTeam, err := command.Flags().GetInt("channels-per-team") if err != nil || channelsPerTeam < 0 { return errors.New("Invalid channels-per-team parameter") } - users, err := cmd.Flags().GetInt("users") + users, err := command.Flags().GetInt("users") if err != nil || users < 0 { return errors.New("Invalid users parameter") } - teamMemberships, err := cmd.Flags().GetInt("team-memberships") + teamMemberships, err := command.Flags().GetInt("team-memberships") if err != nil || teamMemberships < 0 { return errors.New("Invalid team-memberships parameter") } - channelMemberships, err := cmd.Flags().GetInt("channel-memberships") + channelMemberships, err := command.Flags().GetInt("channel-memberships") if err != nil || channelMemberships < 0 { return errors.New("Invalid channel-memberships parameter") } - postsPerChannel, err := cmd.Flags().GetInt("posts-per-channel") + postsPerChannel, err := command.Flags().GetInt("posts-per-channel") if err != nil || postsPerChannel < 0 { return errors.New("Invalid posts-per-channel parameter") } - directChannels, err := cmd.Flags().GetInt("direct-channels") + directChannels, err := command.Flags().GetInt("direct-channels") if err != nil || directChannels < 0 { return errors.New("Invalid direct-channels parameter") } - postsPerDirectChannel, err := cmd.Flags().GetInt("posts-per-direct-channel") + postsPerDirectChannel, err := command.Flags().GetInt("posts-per-direct-channel") if err != nil || postsPerDirectChannel < 0 { return errors.New("Invalid posts-per-direct-channel parameter") } - groupChannels, err := cmd.Flags().GetInt("group-channels") + groupChannels, err := command.Flags().GetInt("group-channels") if err != nil || groupChannels < 0 { return errors.New("Invalid group-channels parameter") } - postsPerGroupChannel, err := cmd.Flags().GetInt("posts-per-group-channel") + postsPerGroupChannel, err := command.Flags().GetInt("posts-per-group-channel") if err != nil || postsPerGroupChannel < 0 { return errors.New("Invalid posts-per-group-channel parameter") } - workers, err := cmd.Flags().GetInt("workers") + workers, err := command.Flags().GetInt("workers") if err != nil { return errors.New("Invalid workers parameter") } - profileImagesPath, err := cmd.Flags().GetString("profile-images") + profileImagesPath, err := command.Flags().GetString("profile-images") if err != nil { return errors.New("Invalid profile-images parameter") } @@ -312,7 +315,7 @@ func sampleDataCmdF(cmd *cobra.Command, args []string) error { } importErr, lineNumber := a.BulkImport(bulkFile, false, workers) if importErr != nil { - return errors.New(fmt.Sprintf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber)) + return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber) } } else if bulk != "-" { err := bulkFile.Close() @@ -395,7 +398,7 @@ func createUser(idx int, teamMemberships int, channelMemberships int, teamsAndCh position := rand.Intn(len(possibleTeams)) team := possibleTeams[position] possibleTeams = append(possibleTeams[:position], possibleTeams[position+1:]...) - if teamChannels, err := teamsAndChannels[team]; err == true { + if teamChannels, err := teamsAndChannels[team]; err { teams = append(teams, createTeamMembership(channelMemberships, teamChannels, &team)) } } @@ -429,10 +432,7 @@ func createTeamMembership(numOfchannels int, teamChannels []string, teamName *st roles = "team_user team_admin" } channels := []app.UserChannelImportData{} - teamChannelsCopy := []string{} - for _, value := range teamChannels { - teamChannelsCopy = append(teamChannelsCopy, value) - } + teamChannelsCopy := append([]string(nil), teamChannels...) for x := 0; x < numOfchannels; x++ { if len(teamChannelsCopy) == 0 { break diff --git a/cmd/platform/sampledata_test.go b/cmd/commands/sampledata_test.go index de28c0856..d71ac0575 100644 --- a/cmd/platform/sampledata_test.go +++ b/cmd/commands/sampledata_test.go @@ -1,12 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "testing" "github.com/mattermost/mattermost-server/api" + "github.com/mattermost/mattermost-server/cmd" "github.com/stretchr/testify/require" ) @@ -15,11 +16,11 @@ func TestSampledataBadParameters(t *testing.T) { defer th.TearDown() // should fail because you need at least 1 worker - require.Error(t, runCommand(t, "sampledata", "--workers", "0")) + require.Error(t, cmd.RunCommand(t, "sampledata", "--workers", "0")) // should fail because you have more team memberships than teams - require.Error(t, runCommand(t, "sampledata", "--teams", "10", "--teams-memberships", "11")) + require.Error(t, cmd.RunCommand(t, "sampledata", "--teams", "10", "--teams-memberships", "11")) // should fail because you have more channel memberships than channels per team - require.Error(t, runCommand(t, "sampledata", "--channels-per-team", "10", "--channel-memberships", "11")) + require.Error(t, cmd.RunCommand(t, "sampledata", "--channels-per-team", "10", "--channel-memberships", "11")) } diff --git a/cmd/platform/server.go b/cmd/commands/server.go index 31606e6eb..8358fe98f 100644 --- a/cmd/platform/server.go +++ b/cmd/commands/server.go @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "net" @@ -14,6 +14,7 @@ import ( "github.com/mattermost/mattermost-server/api" "github.com/mattermost/mattermost-server/api4" "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/manualtesting" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" @@ -31,17 +32,22 @@ var MaxNotificationsPerChannelDefault int64 = 1000000 var serverCmd = &cobra.Command{ Use: "server", Short: "Run the Mattermost server", - RunE: runServerCmd, + RunE: serverCmdF, SilenceUsage: true, } -func runServerCmd(cmd *cobra.Command, args []string) error { - config, err := cmd.Flags().GetString("config") +func init() { + cmd.RootCmd.AddCommand(serverCmd) + cmd.RootCmd.RunE = serverCmdF +} + +func serverCmdF(command *cobra.Command, args []string) error { + config, err := command.Flags().GetString("config") if err != nil { return err } - disableConfigWatch, _ := cmd.Flags().GetBool("disableconfigwatch") + disableConfigWatch, _ := command.Flags().GetBool("disableconfigwatch") interruptChan := make(chan os.Signal, 1) return runServer(config, disableConfigWatch, interruptChan) diff --git a/cmd/platform/server_test.go b/cmd/commands/server_test.go index 2f04e7d15..fb7dfdef2 100644 --- a/cmd/platform/server_test.go +++ b/cmd/commands/server_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "io/ioutil" @@ -110,7 +110,7 @@ func TestRunServerSystemdNotification(t *testing.T) { panic(err) } data := buffer[0:count] - ch<- string(data) + ch <- string(data) }(socketReader) // Start and stop the server diff --git a/cmd/platform/team.go b/cmd/commands/team.go index 1cb5bd99e..9c07b7456 100644 --- a/cmd/platform/team.go +++ b/cmd/commands/team.go @@ -1,22 +1,24 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "errors" "fmt" "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/spf13/cobra" ) -var teamCmd = &cobra.Command{ +var TeamCmd = &cobra.Command{ Use: "team", Short: "Management of teams", } -var teamCreateCmd = &cobra.Command{ +var TeamCreateCmd = &cobra.Command{ Use: "create", Short: "Create a team", Long: `Create a team.`, @@ -25,7 +27,7 @@ var teamCreateCmd = &cobra.Command{ RunE: createTeamCmdF, } -var removeUsersCmd = &cobra.Command{ +var RemoveUsersCmd = &cobra.Command{ Use: "remove [team] [users]", Short: "Remove users from team", Long: "Remove some users from team", @@ -33,7 +35,7 @@ var removeUsersCmd = &cobra.Command{ RunE: removeUsersCmdF, } -var addUsersCmd = &cobra.Command{ +var AddUsersCmd = &cobra.Command{ Use: "add [team] [users]", Short: "Add users to team", Long: "Add some users to team", @@ -41,7 +43,7 @@ var addUsersCmd = &cobra.Command{ RunE: addUsersCmdF, } -var deleteTeamsCmd = &cobra.Command{ +var DeleteTeamsCmd = &cobra.Command{ Use: "delete [teams]", Short: "Delete teams", Long: `Permanently delete some teams. @@ -51,37 +53,38 @@ Permanently deletes a team along with all related information including posts fr } func init() { - teamCreateCmd.Flags().String("name", "", "Team Name") - teamCreateCmd.Flags().String("display_name", "", "Team Display Name") - teamCreateCmd.Flags().Bool("private", false, "Create a private team.") - teamCreateCmd.Flags().String("email", "", "Administrator Email (anyone with this email is automatically a team admin)") - - deleteTeamsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the team and a DB backup has been performed.") - - teamCmd.AddCommand( - teamCreateCmd, - removeUsersCmd, - addUsersCmd, - deleteTeamsCmd, + TeamCreateCmd.Flags().String("name", "", "Team Name") + TeamCreateCmd.Flags().String("display_name", "", "Team Display Name") + TeamCreateCmd.Flags().Bool("private", false, "Create a private team.") + TeamCreateCmd.Flags().String("email", "", "Administrator Email (anyone with this email is automatically a team admin)") + + DeleteTeamsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the team and a DB backup has been performed.") + + TeamCmd.AddCommand( + TeamCreateCmd, + RemoveUsersCmd, + AddUsersCmd, + DeleteTeamsCmd, ) + cmd.RootCmd.AddCommand(TeamCmd) } -func createTeamCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func createTeamCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } - name, errn := cmd.Flags().GetString("name") + name, errn := command.Flags().GetString("name") if errn != nil || name == "" { return errors.New("Name is required") } - displayname, errdn := cmd.Flags().GetString("display_name") + displayname, errdn := command.Flags().GetString("display_name") if errdn != nil || displayname == "" { return errors.New("Display Name is required") } - email, _ := cmd.Flags().GetString("email") - useprivate, _ := cmd.Flags().GetBool("private") + email, _ := command.Flags().GetString("email") + useprivate, _ := command.Flags().GetBool("private") teamType := model.TEAM_OPEN if useprivate { @@ -102,8 +105,8 @@ func createTeamCmdF(cmd *cobra.Command, args []string) error { return nil } -func removeUsersCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func removeUsersCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -127,16 +130,16 @@ func removeUsersCmdF(cmd *cobra.Command, args []string) error { func removeUserFromTeam(a *app.App, team *model.Team, user *model.User, userArg string) { if user == nil { - CommandPrintErrorln("Can't find user '" + userArg + "'") + cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") return } if err := a.LeaveTeam(team, user, ""); err != nil { - CommandPrintErrorln("Unable to remove '" + userArg + "' from " + team.Name + ". Error: " + err.Error()) + cmd.CommandPrintErrorln("Unable to remove '" + userArg + "' from " + team.Name + ". Error: " + err.Error()) } } -func addUsersCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func addUsersCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -160,16 +163,16 @@ func addUsersCmdF(cmd *cobra.Command, args []string) error { func addUserToTeam(a *app.App, team *model.Team, user *model.User, userArg string) { if user == nil { - CommandPrintErrorln("Can't find user '" + userArg + "'") + cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") return } if err := a.JoinUserToTeam(team, user, ""); err != nil { - CommandPrintErrorln("Unable to add '" + userArg + "' to " + team.Name) + cmd.CommandPrintErrorln("Unable to add '" + userArg + "' to " + team.Name) } } -func deleteTeamsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func deleteTeamsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -178,16 +181,16 @@ func deleteTeamsCmdF(cmd *cobra.Command, args []string) error { return errors.New("Not enough arguments.") } - confirmFlag, _ := cmd.Flags().GetBool("confirm") + confirmFlag, _ := command.Flags().GetBool("confirm") if !confirmFlag { var confirm string - CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") fmt.Scanln(&confirm) if confirm != "YES" { return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") } - CommandPrettyPrintln("Are you sure you want to delete the teams specified? All data will be permanently deleted? (YES/NO): ") + cmd.CommandPrettyPrintln("Are you sure you want to delete the teams specified? All data will be permanently deleted? (YES/NO): ") fmt.Scanln(&confirm) if confirm != "YES" { return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") @@ -197,13 +200,13 @@ func deleteTeamsCmdF(cmd *cobra.Command, args []string) error { teams := getTeamsFromTeamArgs(a, args) for i, team := range teams { if team == nil { - CommandPrintErrorln("Unable to find team '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find team '" + args[i] + "'") continue } if err := deleteTeam(a, team); err != nil { - CommandPrintErrorln("Unable to delete team '" + team.Name + "' error: " + err.Error()) + cmd.CommandPrintErrorln("Unable to delete team '" + team.Name + "' error: " + err.Error()) } else { - CommandPrettyPrintln("Deleted team '" + team.Name + "'") + cmd.CommandPrettyPrintln("Deleted team '" + team.Name + "'") } } diff --git a/cmd/platform/team_test.go b/cmd/commands/team_test.go index 1e13d6cfa..1a91df4bc 100644 --- a/cmd/platform/team_test.go +++ b/cmd/commands/team_test.go @@ -1,12 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "testing" "github.com/mattermost/mattermost-server/api" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" ) @@ -18,7 +19,7 @@ func TestCreateTeam(t *testing.T) { name := "name" + id displayName := "Name " + id - checkCommand(t, "team", "create", "--name", name, "--display_name", displayName) + cmd.CheckCommand(t, "team", "create", "--name", name, "--display_name", displayName) found := th.SystemAdminClient.Must(th.SystemAdminClient.FindTeamByName(name)).Data.(bool) @@ -31,7 +32,7 @@ func TestJoinTeam(t *testing.T) { th := api.Setup().InitSystemAdmin().InitBasic() defer th.TearDown() - checkCommand(t, "team", "add", th.SystemAdminTeam.Name, th.BasicUser.Email) + cmd.CheckCommand(t, "team", "add", th.SystemAdminTeam.Name, th.BasicUser.Email) profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesInTeam(th.SystemAdminTeam.Id, 0, 1000, "")).Data.(map[string]*model.User) @@ -53,7 +54,7 @@ func TestLeaveTeam(t *testing.T) { th := api.Setup().InitBasic() defer th.TearDown() - checkCommand(t, "team", "remove", th.BasicTeam.Name, th.BasicUser.Email) + cmd.CheckCommand(t, "team", "remove", th.BasicTeam.Name, th.BasicUser.Email) profiles := th.BasicClient.Must(th.BasicClient.GetProfilesInTeam(th.BasicTeam.Id, 0, 1000, "")).Data.(map[string]*model.User) diff --git a/cmd/platform/teamargs.go b/cmd/commands/teamargs.go index 144db388b..aa62d52b8 100644 --- a/cmd/platform/teamargs.go +++ b/cmd/commands/teamargs.go @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "github.com/mattermost/mattermost-server/app" diff --git a/cmd/platform/test.go b/cmd/commands/test.go index 9ab3fbb36..62df16438 100644 --- a/cmd/platform/test.go +++ b/cmd/commands/test.go @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "bufio" @@ -14,39 +14,41 @@ import ( "github.com/mattermost/mattermost-server/api" "github.com/mattermost/mattermost-server/api4" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/wsapi" "github.com/spf13/cobra" ) -var testCmd = &cobra.Command{ +var TestCmd = &cobra.Command{ Use: "test", Short: "Testing Commands", Hidden: true, } -var runWebClientTestsCmd = &cobra.Command{ +var RunWebClientTestsCmd = &cobra.Command{ Use: "web_client_tests", Short: "Run the web client tests", RunE: webClientTestsCmdF, } -var runServerForWebClientTestsCmd = &cobra.Command{ +var RunServerForWebClientTestsCmd = &cobra.Command{ Use: "web_client_tests_server", Short: "Run the server configured for running the web client tests against it", RunE: serverForWebClientTestsCmdF, } func init() { - testCmd.AddCommand( - runWebClientTestsCmd, - runServerForWebClientTestsCmd, + TestCmd.AddCommand( + RunWebClientTestsCmd, + RunServerForWebClientTestsCmd, ) + cmd.RootCmd.AddCommand(TestCmd) } -func webClientTestsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func webClientTestsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -67,8 +69,8 @@ func webClientTestsCmdF(cmd *cobra.Command, args []string) error { return nil } -func serverForWebClientTestsCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func serverForWebClientTestsCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -101,17 +103,17 @@ func setupClientTests(cfg *model.Config) { cfg.ServiceSettings.EnableOutgoingWebhooks = false } -func executeTestCommand(cmd *exec.Cmd) { - cmdOutPipe, err := cmd.StdoutPipe() +func executeTestCommand(command *exec.Cmd) { + cmdOutPipe, err := command.StdoutPipe() if err != nil { - CommandPrintErrorln("Failed to run tests") + cmd.CommandPrintErrorln("Failed to run tests") os.Exit(1) return } - cmdErrOutPipe, err := cmd.StderrPipe() + cmdErrOutPipe, err := command.StderrPipe() if err != nil { - CommandPrintErrorln("Failed to run tests") + cmd.CommandPrintErrorln("Failed to run tests") os.Exit(1) return } @@ -130,8 +132,8 @@ func executeTestCommand(cmd *exec.Cmd) { } }() - if err := cmd.Run(); err != nil { - CommandPrintErrorln("Client Tests failed") + if err := command.Run(); err != nil { + cmd.CommandPrintErrorln("Client Tests failed") os.Exit(1) return } diff --git a/cmd/platform/user.go b/cmd/commands/user.go index edbccb164..fe4d34a48 100644 --- a/cmd/platform/user.go +++ b/cmd/commands/user.go @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "encoding/json" @@ -10,16 +11,17 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/spf13/cobra" ) -var userCmd = &cobra.Command{ +var UserCmd = &cobra.Command{ Use: "user", Short: "Management of users", } -var userActivateCmd = &cobra.Command{ +var UserActivateCmd = &cobra.Command{ Use: "activate [emails, usernames, userIds]", Short: "Activate users", Long: "Activate users that have been deactivated.", @@ -28,7 +30,7 @@ var userActivateCmd = &cobra.Command{ RunE: userActivateCmdF, } -var userDeactivateCmd = &cobra.Command{ +var UserDeactivateCmd = &cobra.Command{ Use: "deactivate [emails, usernames, userIds]", Short: "Deactivate users", Long: "Deactivate users. Deactivated users are immediately logged out of all sessions and are unable to log back in.", @@ -37,7 +39,7 @@ var userDeactivateCmd = &cobra.Command{ RunE: userDeactivateCmdF, } -var userCreateCmd = &cobra.Command{ +var UserCreateCmd = &cobra.Command{ Use: "create", Short: "Create a user", Long: "Create a user", @@ -45,7 +47,7 @@ var userCreateCmd = &cobra.Command{ RunE: userCreateCmdF, } -var userInviteCmd = &cobra.Command{ +var UserInviteCmd = &cobra.Command{ Use: "invite [email] [teams]", Short: "Send user an email invite to a team.", Long: `Send user an email invite to a team. @@ -56,7 +58,7 @@ You can specify teams by name or ID.`, RunE: userInviteCmdF, } -var resetUserPasswordCmd = &cobra.Command{ +var ResetUserPasswordCmd = &cobra.Command{ Use: "password [user] [password]", Short: "Set a user's password", Long: "Set a user's password", @@ -64,7 +66,16 @@ var resetUserPasswordCmd = &cobra.Command{ RunE: resetUserPasswordCmdF, } -var resetUserMfaCmd = &cobra.Command{ +var updateUserEmailCmd = &cobra.Command{ + Use: "email [user] [new email]", + Short: "Change email of the user", + Long: "Change email of the user.", + Example: ` user email test user@example.com + user activate username`, + RunE: updateUserEmailCmdF, +} + +var ResetUserMfaCmd = &cobra.Command{ Use: "resetmfa [users]", Short: "Turn off MFA", Long: `Turn off multi-factor authentication for a user. @@ -73,7 +84,7 @@ If MFA enforcement is enabled, the user will be forced to re-enable MFA as soon RunE: resetUserMfaCmdF, } -var deleteUserCmd = &cobra.Command{ +var DeleteUserCmd = &cobra.Command{ Use: "delete [users]", Short: "Delete users and all posts", Long: "Permanently delete user and all related information including posts.", @@ -81,7 +92,7 @@ var deleteUserCmd = &cobra.Command{ RunE: deleteUserCmdF, } -var deleteAllUsersCmd = &cobra.Command{ +var DeleteAllUsersCmd = &cobra.Command{ Use: "deleteall", Short: "Delete all users and all posts", Long: "Permanently delete all users and all related information including posts.", @@ -89,7 +100,7 @@ var deleteAllUsersCmd = &cobra.Command{ RunE: deleteAllUsersCommandF, } -var migrateAuthCmd = &cobra.Command{ +var MigrateAuthCmd = &cobra.Command{ Use: "migrate_auth [from_auth] [to_auth] [migration-options]", Short: "Mass migrate user accounts authentication type", Long: `Migrates accounts from one authentication provider to another. For example, you can upgrade your authentication provider from email to ldap.`, @@ -127,7 +138,7 @@ var migrateAuthCmd = &cobra.Command{ RunE: migrateAuthCmdF, } -var verifyUserCmd = &cobra.Command{ +var VerifyUserCmd = &cobra.Command{ Use: "verify [users]", Short: "Verify email of users", Long: "Verify the emails of some users.", @@ -135,7 +146,7 @@ var verifyUserCmd = &cobra.Command{ RunE: verifyUserCmdF, } -var searchUserCmd = &cobra.Command{ +var SearchUserCmd = &cobra.Command{ Use: "search [users]", Short: "Search for users", Long: "Search for users based on username, email, or user ID.", @@ -144,23 +155,23 @@ var searchUserCmd = &cobra.Command{ } func init() { - userCreateCmd.Flags().String("username", "", "Required. Username for the new user account.") - userCreateCmd.Flags().String("email", "", "Required. The email address for the new user account.") - userCreateCmd.Flags().String("password", "", "Required. The password for the new user account.") - userCreateCmd.Flags().String("nickname", "", "Optional. The nickname for the new user account.") - userCreateCmd.Flags().String("firstname", "", "Optional. The first name for the new user account.") - userCreateCmd.Flags().String("lastname", "", "Optional. The last name for the new user account.") - userCreateCmd.Flags().String("locale", "", "Optional. The locale (ex: en, fr) for the new user account.") - userCreateCmd.Flags().Bool("system_admin", false, "Optional. If supplied, the new user will be a system administrator. Defaults to false.") - - deleteUserCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") - - deleteAllUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") - - migrateAuthCmd.Flags().Bool("force", false, "Force the migration to occur even if there are duplicates on the LDAP server. Duplicates will not be migrated. (ldap only)") - migrateAuthCmd.Flags().Bool("auto", false, "Automatically migrate all users. Assumes the usernames and emails are identical between Mattermost and SAML services. (saml only)") - migrateAuthCmd.Flags().Bool("dryRun", false, "Run a simulation of the migration process without changing the database.") - migrateAuthCmd.SetUsageTemplate(`Usage: + UserCreateCmd.Flags().String("username", "", "Required. Username for the new user account.") + UserCreateCmd.Flags().String("email", "", "Required. The email address for the new user account.") + UserCreateCmd.Flags().String("password", "", "Required. The password for the new user account.") + UserCreateCmd.Flags().String("nickname", "", "Optional. The nickname for the new user account.") + UserCreateCmd.Flags().String("firstname", "", "Optional. The first name for the new user account.") + UserCreateCmd.Flags().String("lastname", "", "Optional. The last name for the new user account.") + UserCreateCmd.Flags().String("locale", "", "Optional. The locale (ex: en, fr) for the new user account.") + UserCreateCmd.Flags().Bool("system_admin", false, "Optional. If supplied, the new user will be a system administrator. Defaults to false.") + + DeleteUserCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") + + DeleteAllUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") + + MigrateAuthCmd.Flags().Bool("force", false, "Force the migration to occur even if there are duplicates on the LDAP server. Duplicates will not be migrated. (ldap only)") + MigrateAuthCmd.Flags().Bool("auto", false, "Automatically migrate all users. Assumes the usernames and emails are identical between Mattermost and SAML services. (saml only)") + MigrateAuthCmd.Flags().Bool("dryRun", false, "Run a simulation of the migration process without changing the database.") + MigrateAuthCmd.SetUsageTemplate(`Usage: platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags] Examples: @@ -184,7 +195,7 @@ Flags: Global Flags: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} `) - migrateAuthCmd.SetHelpTemplate(`Usage: + MigrateAuthCmd.SetHelpTemplate(`Usage: platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags] Examples: @@ -221,23 +232,25 @@ Global Flags: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} `) - userCmd.AddCommand( - userActivateCmd, - userDeactivateCmd, - userCreateCmd, - userInviteCmd, - resetUserPasswordCmd, - resetUserMfaCmd, - deleteUserCmd, - deleteAllUsersCmd, - migrateAuthCmd, - verifyUserCmd, - searchUserCmd, + UserCmd.AddCommand( + UserActivateCmd, + UserDeactivateCmd, + UserCreateCmd, + UserInviteCmd, + ResetUserPasswordCmd, + updateUserEmailCmd, + ResetUserMfaCmd, + DeleteUserCmd, + DeleteAllUsersCmd, + MigrateAuthCmd, + VerifyUserCmd, + SearchUserCmd, ) + cmd.RootCmd.AddCommand(UserCmd) } -func userActivateCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func userActivateCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -256,7 +269,7 @@ func changeUsersActiveStatus(a *app.App, userArgs []string, active bool) { err := changeUserActiveStatus(a, user, userArgs[i], active) if err != nil { - CommandPrintErrorln(err.Error()) + cmd.CommandPrintErrorln(err.Error()) } } } @@ -275,8 +288,8 @@ func changeUserActiveStatus(a *app.App, user *model.User, userArg string, activa return nil } -func userDeactivateCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func userDeactivateCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -289,29 +302,29 @@ func userDeactivateCmdF(cmd *cobra.Command, args []string) error { return nil } -func userCreateCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func userCreateCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } - username, erru := cmd.Flags().GetString("username") + username, erru := command.Flags().GetString("username") if erru != nil || username == "" { return errors.New("Username is required") } - email, erre := cmd.Flags().GetString("email") + email, erre := command.Flags().GetString("email") if erre != nil || email == "" { return errors.New("Email is required") } - password, errp := cmd.Flags().GetString("password") + password, errp := command.Flags().GetString("password") if errp != nil || password == "" { return errors.New("Password is required") } - nickname, _ := cmd.Flags().GetString("nickname") - firstname, _ := cmd.Flags().GetString("firstname") - lastname, _ := cmd.Flags().GetString("lastname") - locale, _ := cmd.Flags().GetString("locale") - systemAdmin, _ := cmd.Flags().GetBool("system_admin") + nickname, _ := command.Flags().GetString("nickname") + firstname, _ := command.Flags().GetString("firstname") + lastname, _ := command.Flags().GetString("lastname") + locale, _ := command.Flags().GetString("locale") + systemAdmin, _ := command.Flags().GetBool("system_admin") user := &model.User{ Username: username, @@ -329,13 +342,13 @@ func userCreateCmdF(cmd *cobra.Command, args []string) error { a.UpdateUserRoles(ruser.Id, "system_user system_admin", false) } - CommandPrettyPrintln("Created User") + cmd.CommandPrettyPrintln("Created User") return nil } -func userInviteCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func userInviteCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -354,7 +367,7 @@ func userInviteCmdF(cmd *cobra.Command, args []string) error { err := inviteUser(a, email, team, args[i+1]) if err != nil { - CommandPrintErrorln(err.Error()) + cmd.CommandPrintErrorln(err.Error()) } } @@ -368,13 +381,13 @@ func inviteUser(a *app.App, email string, team *model.Team, teamArg string) erro } a.SendInviteEmails(team, "Administrator", invites, *a.Config().ServiceSettings.SiteURL) - CommandPrettyPrintln("Invites may or may not have been sent.") + cmd.CommandPrettyPrintln("Invites may or may not have been sent.") return nil } -func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func resetUserPasswordCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -396,8 +409,38 @@ func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error { return nil } -func resetUserMfaCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func updateUserEmailCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) + if err != nil { + return err + } + + newEmail := args[1] + + if !model.IsValidEmail(newEmail) { + return errors.New("Invalid email: '" + newEmail + "'") + } + + if len(args) != 2 { + return errors.New("Expected two arguments. See help text for details.") + } + + user := getUserFromUserArg(a, args[0]) + if user == nil { + return errors.New("Unable to find user '" + args[0] + "'") + } + + user.Email = newEmail + _, errUpdate := a.UpdateUser(user, true) + if err != nil { + return errUpdate + } + + return nil +} + +func resetUserMfaCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -421,8 +464,8 @@ func resetUserMfaCmdF(cmd *cobra.Command, args []string) error { return nil } -func deleteUserCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func deleteUserCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -431,16 +474,16 @@ func deleteUserCmdF(cmd *cobra.Command, args []string) error { return errors.New("Expected at least one argument. See help text for details.") } - confirmFlag, _ := cmd.Flags().GetBool("confirm") + confirmFlag, _ := command.Flags().GetBool("confirm") if !confirmFlag { var confirm string - CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") fmt.Scanln(&confirm) if confirm != "YES" { return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") } - CommandPrettyPrintln("Are you sure you want to permanently delete the specified users? (YES/NO): ") + cmd.CommandPrettyPrintln("Are you sure you want to permanently delete the specified users? (YES/NO): ") fmt.Scanln(&confirm) if confirm != "YES" { return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") @@ -462,8 +505,8 @@ func deleteUserCmdF(cmd *cobra.Command, args []string) error { return nil } -func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func deleteAllUsersCommandF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -472,16 +515,16 @@ func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error { return errors.New("Expected zero arguments.") } - confirmFlag, _ := cmd.Flags().GetBool("confirm") + confirmFlag, _ := command.Flags().GetBool("confirm") if !confirmFlag { var confirm string - CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") fmt.Scanln(&confirm) if confirm != "YES" { return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") } - CommandPrettyPrintln("Are you sure you want to permanently delete all user accounts? (YES/NO): ") + cmd.CommandPrettyPrintln("Are you sure you want to permanently delete all user accounts? (YES/NO): ") fmt.Scanln(&confirm) if confirm != "YES" { return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") @@ -492,19 +535,19 @@ func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error { return err } - CommandPrettyPrintln("All user accounts successfully deleted.") + cmd.CommandPrettyPrintln("All user accounts successfully deleted.") return nil } -func migrateAuthCmdF(cmd *cobra.Command, args []string) error { +func migrateAuthCmdF(command *cobra.Command, args []string) error { if args[1] == "saml" { - return migrateAuthToSamlCmdF(cmd, args) + return migrateAuthToSamlCmdF(command, args) } - return migrateAuthToLdapCmdF(cmd, args) + return migrateAuthToLdapCmdF(command, args) } -func migrateAuthToLdapCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func migrateAuthToLdapCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -525,28 +568,28 @@ func migrateAuthToLdapCmdF(cmd *cobra.Command, args []string) error { return errors.New("Invalid match_field argument") } - forceFlag, _ := cmd.Flags().GetBool("force") - dryRunFlag, _ := cmd.Flags().GetBool("dryRun") + forceFlag, _ := command.Flags().GetBool("force") + dryRunFlag, _ := command.Flags().GetBool("dryRun") if migrate := a.AccountMigration; migrate != nil { if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag, dryRunFlag); err != nil { return errors.New("Error while migrating users: " + err.Error()) } - CommandPrettyPrintln("Sucessfully migrated accounts.") + cmd.CommandPrettyPrintln("Sucessfully migrated accounts.") } return nil } -func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func migrateAuthToSamlCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } - dryRunFlag, _ := cmd.Flags().GetBool("dryRun") - autoFlag, _ := cmd.Flags().GetBool("auto") + dryRunFlag, _ := command.Flags().GetBool("dryRun") + autoFlag, _ := command.Flags().GetBool("auto") matchesFile := "" matches := map[string]string{} @@ -570,7 +613,7 @@ func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error { if autoFlag && !dryRunFlag { var confirm string - CommandPrettyPrintln("You are about to perform an automatic \"" + fromAuth + " to saml\" migration. This must only be done if your current Mattermost users with " + fromAuth + " auth have the same username and email in your SAML service. Otherwise, provide the usernames and emails from your SAML Service using the \"users file\" without the \"--auto\" option.\n\nDo you want to proceed with automatic migration anyway? (YES/NO):") + cmd.CommandPrettyPrintln("You are about to perform an automatic \"" + fromAuth + " to saml\" migration. This must only be done if your current Mattermost users with " + fromAuth + " auth have the same username and email in your SAML service. Otherwise, provide the usernames and emails from your SAML Service using the \"users file\" without the \"--auto\" option.\n\nDo you want to proceed with automatic migration anyway? (YES/NO):") fmt.Scanln(&confirm) if confirm != "YES" { @@ -588,14 +631,14 @@ func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error { return errors.New("Error while migrating users: " + err.Error()) } l4g.Close() - CommandPrettyPrintln("Sucessfully migrated accounts.") + cmd.CommandPrettyPrintln("Sucessfully migrated accounts.") } return nil } -func verifyUserCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func verifyUserCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -608,19 +651,19 @@ func verifyUserCmdF(cmd *cobra.Command, args []string) error { for i, user := range users { if user == nil { - CommandPrintErrorln("Unable to find user '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find user '" + args[i] + "'") continue } if cresult := <-a.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil { - CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error()) + cmd.CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error()) } } return nil } -func searchUserCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func searchUserCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -633,21 +676,21 @@ func searchUserCmdF(cmd *cobra.Command, args []string) error { for i, user := range users { if i > 0 { - CommandPrettyPrintln("------------------------------") + cmd.CommandPrettyPrintln("------------------------------") } if user == nil { - CommandPrintErrorln("Unable to find user '" + args[i] + "'") + cmd.CommandPrintErrorln("Unable to find user '" + args[i] + "'") continue } - CommandPrettyPrintln("id: " + user.Id) - CommandPrettyPrintln("username: " + user.Username) - CommandPrettyPrintln("nickname: " + user.Nickname) - CommandPrettyPrintln("position: " + user.Position) - CommandPrettyPrintln("first_name: " + user.FirstName) - CommandPrettyPrintln("last_name: " + user.LastName) - CommandPrettyPrintln("email: " + user.Email) - CommandPrettyPrintln("auth_service: " + user.AuthService) + cmd.CommandPrettyPrintln("id: " + user.Id) + cmd.CommandPrettyPrintln("username: " + user.Username) + cmd.CommandPrettyPrintln("nickname: " + user.Nickname) + cmd.CommandPrettyPrintln("position: " + user.Position) + cmd.CommandPrettyPrintln("first_name: " + user.FirstName) + cmd.CommandPrettyPrintln("last_name: " + user.LastName) + cmd.CommandPrettyPrintln("email: " + user.Email) + cmd.CommandPrettyPrintln("auth_service: " + user.AuthService) } return nil diff --git a/cmd/platform/user_test.go b/cmd/commands/user_test.go index 5383ad914..a1081c5d3 100644 --- a/cmd/platform/user_test.go +++ b/cmd/commands/user_test.go @@ -1,13 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "testing" "github.com/mattermost/mattermost-server/api" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" + "github.com/stretchr/testify/require" ) func TestCreateUserWithTeam(t *testing.T) { @@ -18,9 +20,9 @@ func TestCreateUserWithTeam(t *testing.T) { email := "success+" + id + "@simulator.amazonses.com" username := "name" + id - checkCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) + cmd.CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) - checkCommand(t, "team", "add", th.SystemAdminTeam.Id, email) + cmd.CheckCommand(t, "team", "add", th.SystemAdminTeam.Id, email) profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesInTeam(th.SystemAdminTeam.Id, 0, 1000, "")).Data.(map[string]*model.User) @@ -46,7 +48,7 @@ func TestCreateUserWithoutTeam(t *testing.T) { email := "success+" + id + "@simulator.amazonses.com" username := "name" + id - checkCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) + cmd.CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) if result := <-th.App.Srv.Store.User().GetByEmail(email); result.Err != nil { t.Fatal() @@ -62,7 +64,7 @@ func TestResetPassword(t *testing.T) { th := api.Setup().InitBasic() defer th.TearDown() - checkCommand(t, "user", "password", th.BasicUser.Email, "password2") + cmd.CheckCommand(t, "user", "password", th.BasicUser.Email, "password2") th.BasicClient.Logout() th.BasicUser.Password = "password2" @@ -74,8 +76,35 @@ func TestMakeUserActiveAndInactive(t *testing.T) { defer th.TearDown() // first inactivate the user - checkCommand(t, "user", "deactivate", th.BasicUser.Email) + cmd.CheckCommand(t, "user", "deactivate", th.BasicUser.Email) // activate the inactive user - checkCommand(t, "user", "activate", th.BasicUser.Email) + cmd.CheckCommand(t, "user", "activate", th.BasicUser.Email) +} + +func TestChangeUserEmail(t *testing.T) { + th := api.Setup().InitBasic() + defer th.TearDown() + + newEmail := model.NewId() + "@mattermost-test.com" + + cmd.CheckCommand(t, "user", "email", th.BasicUser.Username, newEmail) + if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err == nil { + t.Fatal("should've updated to the new email") + } + if result := <-th.App.Srv.Store.User().GetByEmail(newEmail); result.Err != nil { + t.Fatal() + } else { + user := result.Data.(*model.User) + if user.Email != newEmail { + t.Fatal("should've updated to the new email") + } + } + + // should fail because using an invalid email + require.Error(t, cmd.RunCommand(t, "user", "email", th.BasicUser.Username, "wrong$email.com")) + + // should fail because user not found + require.Error(t, cmd.RunCommand(t, "user", "email", "invalidUser", newEmail)) + } diff --git a/cmd/platform/userargs.go b/cmd/commands/userargs.go index 0089cc4da..ddeed6460 100644 --- a/cmd/platform/userargs.go +++ b/cmd/commands/userargs.go @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "github.com/mattermost/mattermost-server/app" diff --git a/cmd/platform/version.go b/cmd/commands/version.go index 9616be1d7..eaf6a1a68 100644 --- a/cmd/platform/version.go +++ b/cmd/commands/version.go @@ -1,23 +1,29 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package commands import ( "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/store/sqlstore" "github.com/spf13/cobra" ) -var versionCmd = &cobra.Command{ +var VersionCmd = &cobra.Command{ Use: "version", Short: "Display version information", RunE: versionCmdF, } -func versionCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) +func init() { + cmd.RootCmd.AddCommand(VersionCmd) +} + +func versionCmdF(command *cobra.Command, args []string) error { + a, err := cmd.InitDBCommandContextCobra(command) if err != nil { return err } @@ -28,12 +34,12 @@ func versionCmdF(cmd *cobra.Command, args []string) error { } func printVersion(a *app.App) { - CommandPrintln("Version: " + model.CurrentVersion) - CommandPrintln("Build Number: " + model.BuildNumber) - CommandPrintln("Build Date: " + model.BuildDate) - CommandPrintln("Build Hash: " + model.BuildHash) - CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady) + cmd.CommandPrintln("Version: " + model.CurrentVersion) + cmd.CommandPrintln("Build Number: " + model.BuildNumber) + cmd.CommandPrintln("Build Date: " + model.BuildDate) + cmd.CommandPrintln("Build Hash: " + model.BuildHash) + cmd.CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady) if supplier, ok := a.Srv.Store.(*store.LayeredStore).DatabaseLayer.(*sqlstore.SqlSupplier); ok { - CommandPrintln("DB Version: " + supplier.GetCurrentSchemaVersion()) + cmd.CommandPrintln("DB Version: " + supplier.GetCurrentSchemaVersion()) } } diff --git a/cmd/platform/version_test.go b/cmd/commands/version_test.go index eea2549ee..24a1389b1 100644 --- a/cmd/platform/version_test.go +++ b/cmd/commands/version_test.go @@ -1,12 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package commands import ( "testing" + + "github.com/mattermost/mattermost-server/cmd" ) func TestVersion(t *testing.T) { - checkCommand(t, "version") + cmd.CheckCommand(t, "version") } diff --git a/cmd/platform/init.go b/cmd/init.go index ef3d78692..b71d71d31 100644 --- a/cmd/platform/init.go +++ b/cmd/init.go @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main +package cmd import ( "github.com/mattermost/mattermost-server/app" @@ -10,13 +10,13 @@ import ( "github.com/spf13/cobra" ) -func initDBCommandContextCobra(cmd *cobra.Command) (*app.App, error) { +func InitDBCommandContextCobra(cmd *cobra.Command) (*app.App, error) { config, err := cmd.Flags().GetString("config") if err != nil { return nil, err } - a, err := initDBCommandContext(config) + a, err := InitDBCommandContext(config) if err != nil { // Returning an error just prints the usage message, so actually panic panic(err) @@ -25,7 +25,7 @@ func initDBCommandContextCobra(cmd *cobra.Command) (*app.App, error) { return a, nil } -func initDBCommandContext(configFileLocation string) (*app.App, error) { +func InitDBCommandContext(configFileLocation string) (*app.App, error) { if err := utils.TranslationsPreInit(); err != nil { return nil, err } diff --git a/cmd/platform/output.go b/cmd/output.go index edf6ccc71..630e831de 100644 --- a/cmd/platform/output.go +++ b/cmd/output.go @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -package main + +package cmd import ( "fmt" diff --git a/cmd/platform/mattermost.go b/cmd/platform/mattermost.go deleted file mode 100644 index e4a120e1e..000000000 --- a/cmd/platform/mattermost.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package main - -import ( - "errors" - "fmt" - "os" - - "github.com/spf13/cobra" - - // Plugins - _ "github.com/mattermost/mattermost-server/model/gitlab" - - // Enterprise Imports - _ "github.com/mattermost/mattermost-server/imports" - - // Enterprise Deps - _ "github.com/dgryski/dgoogauth" - _ "github.com/go-ldap/ldap" - _ "github.com/hashicorp/memberlist" - _ "github.com/mattermost/rsc/qr" - _ "github.com/prometheus/client_golang/prometheus" - _ "github.com/prometheus/client_golang/prometheus/promhttp" - _ "github.com/tylerb/graceful" - _ "gopkg.in/olivere/elastic.v5" - - // Temp imports for new dependencies - _ "github.com/gorilla/schema" -) - -func main() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } -} - -func init() { - rootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.") - rootCmd.PersistentFlags().Bool("disableconfigwatch", false, "When set config.json will not be loaded from disk when the file is changed.") - - resetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.") - - rootCmd.AddCommand(serverCmd, versionCmd, userCmd, teamCmd, licenseCmd, importCmd, resetCmd, channelCmd, rolesCmd, testCmd, ldapCmd, configCmd, jobserverCmd, commandCmd, messageExportCmd, sampleDataCmd) -} - -var rootCmd = &cobra.Command{ - Use: "platform", - Short: "Open source, self-hosted Slack-alternative", - Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`, - RunE: runServerCmd, -} - -var resetCmd = &cobra.Command{ - Use: "reset", - Short: "Reset the database to initial state", - Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.", - RunE: resetCmdF, -} - -func resetCmdF(cmd *cobra.Command, args []string) error { - a, err := initDBCommandContextCobra(cmd) - if err != nil { - return err - } - - confirmFlag, _ := cmd.Flags().GetBool("confirm") - if !confirmFlag { - var confirm string - CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") - fmt.Scanln(&confirm) - - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ") - fmt.Scanln(&confirm) - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - a.Srv.Store.DropAllTables() - CommandPrettyPrintln("Database sucessfully reset") - - return nil -} diff --git a/config/default.json b/config/default.json index cd170adef..b6bcc270a 100644 --- a/config/default.json +++ b/config/default.json @@ -358,7 +358,13 @@ "DailyRunTime": "01:00", "ExportFromTimestamp": 0, "FileLocation": "export", - "BatchSize": 10000 + "BatchSize": 10000, + "GlobalRelaySettings": { + "CustomerType": "A9", + "SmtpUsername": "", + "SmtpPassword": "", + "EmailAddress": "" + } }, "JobSettings": { "RunJobs": true, diff --git a/einterfaces/metrics.go b/einterfaces/metrics.go index a88fe63cf..3f709eb99 100644 --- a/einterfaces/metrics.go +++ b/einterfaces/metrics.go @@ -29,8 +29,10 @@ type MetricsInterface interface { IncrementMemCacheHitCounter(cacheName string) IncrementMemCacheMissCounter(cacheName string) + IncrementMemCacheInvalidationCounter(cacheName string) IncrementMemCacheMissCounterSession() IncrementMemCacheHitCounterSession() + IncrementMemCacheInvalidationCounterSession() IncrementWebsocketEvent(eventType string) IncrementWebSocketBroadcast(eventType string) diff --git a/glide.lock b/glide.lock index 2efc6ba21..4349066e2 100644 --- a/glide.lock +++ b/glide.lock @@ -1,11 +1,13 @@ -hash: 6779beaa11fdb9c520471fb87c0a1a6ecc34a4c82610d942c44fba2f27a29936 -updated: 2018-02-15T18:28:32.209282461-08:00 +hash: 822849f55f8ab4b5c7545597b209edb6114bcf1009a552a9ee2503ff8d3fda09 +updated: 2018-03-07T13:01:49.575101746+01:00 imports: - name: github.com/alecthomas/log4go version: 3fbce08846379ec7f4f6bc7fce6dd01ce28fae4c repo: https://github.com/mattermost/log4go.git - name: github.com/armon/go-metrics version: 7aa49fde808223f8dadfdbfd3a20ff6c19e5f9ec +- name: github.com/avct/uasurfer + version: c4be5581ec9617d04f5c5e02b893903ead0b1eed - name: github.com/beorn7/perks version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 subpackages: @@ -138,8 +140,6 @@ imports: version: b8bc1bf767474819792c23f32d8286a45736f1c6 - name: github.com/mitchellh/mapstructure version: a4e142e9c047c904fa2f1e144d9a84e6133024bc -- name: github.com/mssola/user_agent - version: 5243daae23628aeae9b6268541406bd5e95d5964 - name: github.com/nicksnyder/go-i18n version: 0dc1626d56435e9d605a29875701721c54bc9bbd subpackages: diff --git a/glide.yaml b/glide.yaml index dc67106b4..02889a57b 100644 --- a/glide.yaml +++ b/glide.yaml @@ -77,3 +77,4 @@ import: subpackages: - store/memstore - package: gopkg.in/yaml.v2 +- package: github.com/avct/uasurfer diff --git a/i18n/en.json b/i18n/en.json index 8457d6d3d..ed4d9dcf6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -108,6 +108,18 @@ "translation": "Mattermost - Testing Email Settings" }, { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Empty array under 'image' in request" }, @@ -2187,6 +2199,50 @@ "translation": "The number of running goroutines is over the health threshold %v of %v" }, { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Unable to upload team icon. Image storage is not configured." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Unable to upload team icon. File is too large." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Could not parse multipart form" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "No file under 'image' in request" + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Empty array under 'image' in request" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Could not open image file" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, + { "id": "api.team.add_user_to_team.added", "translation": "%v added to the team by %v." }, @@ -4891,6 +4947,30 @@ "translation": "Message export job BatchSize must be a positive integer" }, { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { "id": "model.config.is_valid.message_export.daily_runtime.app_error", "translation": "Message export job DailyRuntime must be a 24-hour time stamp in the form HH:MM." }, diff --git a/jobs/testworker.go b/jobs/testworker.go deleted file mode 100644 index 9cfc8614f..000000000 --- a/jobs/testworker.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package jobs - -import ( - "context" - "time" - - l4g "github.com/alecthomas/log4go" - "github.com/mattermost/mattermost-server/model" -) - -type TestWorker struct { - srv *JobServer - name string - stop chan bool - stopped chan bool - jobs chan model.Job -} - -func (srv *JobServer) MakeTestWorker(name string) *TestWorker { - return &TestWorker{ - srv: srv, - name: name, - stop: make(chan bool, 1), - stopped: make(chan bool, 1), - jobs: make(chan model.Job), - } -} - -func (worker *TestWorker) Run() { - l4g.Debug("Worker %v: Started", worker.name) - - defer func() { - l4g.Debug("Worker %v: Finished", worker.name) - worker.stopped <- true - }() - - for { - select { - case <-worker.stop: - l4g.Debug("Worker %v: Received stop signal", worker.name) - return - case job := <-worker.jobs: - l4g.Debug("Worker %v: Received a new candidate job.", worker.name) - worker.DoJob(&job) - } - } -} - -func (worker *TestWorker) DoJob(job *model.Job) { - if claimed, err := worker.srv.ClaimJob(job); err != nil { - l4g.Error("Job: %v: Error occurred while trying to claim job: %v", job.Id, err.Error()) - return - } else if !claimed { - return - } - - cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background()) - cancelWatcherChan := make(chan interface{}, 1) - go worker.srv.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan) - - defer cancelCancelWatcher() - - counter := 0 - for { - select { - case <-cancelWatcherChan: - l4g.Debug("Job %v: Job has been canceled via CancellationWatcher.", job.Id) - if err := worker.srv.SetJobCanceled(job); err != nil { - l4g.Error("Failed to mark job: %v as canceled. Error: %v", job.Id, err.Error()) - } - return - case <-worker.stop: - l4g.Debug("Job %v: Job has been canceled via Worker Stop.", job.Id) - if err := worker.srv.SetJobCanceled(job); err != nil { - l4g.Error("Failed to mark job: %v as canceled. Error: %v", job.Id, err.Error()) - } - return - case <-time.After(5 * time.Second): - counter++ - if counter > 10 { - l4g.Debug("Job %v: Job completed.", job.Id) - if err := worker.srv.SetJobSuccess(job); err != nil { - l4g.Error("Failed to mark job: %v as succeeded. Error: %v", job.Id, err.Error()) - } - return - } else { - if err := worker.srv.SetJobProgress(job, int64(counter*10)); err != nil { - l4g.Error("Job: %v: an error occured while trying to set job progress: %v", job.Id, err.Error()) - } - } - } - } -} - -func (worker *TestWorker) Stop() { - l4g.Debug("Worker %v: Stopping", worker.name) - worker.stop <- true - <-worker.stopped -} - -func (worker *TestWorker) JobChannel() chan<- model.Job { - return worker.jobs -} diff --git a/main.go b/main.go new file mode 100644 index 000000000..3761f9255 --- /dev/null +++ b/main.go @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package main + +import ( + "os" + + "github.com/mattermost/mattermost-server/cmd" + _ "github.com/mattermost/mattermost-server/cmd/commands" + + // Plugins + _ "github.com/mattermost/mattermost-server/model/gitlab" + + // Enterprise Imports + _ "github.com/mattermost/mattermost-server/imports" + + // Enterprise Deps + _ "github.com/dgryski/dgoogauth" + _ "github.com/go-ldap/ldap" + _ "github.com/hashicorp/memberlist" + _ "github.com/mattermost/rsc/qr" + _ "github.com/prometheus/client_golang/prometheus" + _ "github.com/prometheus/client_golang/prometheus/promhttp" + _ "github.com/tylerb/graceful" + _ "gopkg.in/olivere/elastic.v5" + + // Temp imports for new dependencies + _ "github.com/gorilla/schema" +) + +func main() { + if err := cmd.Run(os.Args[1:]); err != nil { + os.Exit(1) + } +} diff --git a/model/authorization.go b/model/authorization.go index 9f4e36eab..34faf8bba 100644 --- a/model/authorization.go +++ b/model/authorization.go @@ -503,19 +503,6 @@ func initializeDefaultRoles() { } } -func RoleIdsToString(roles []string) string { - output := "" - for _, role := range roles { - output += role + ", " - } - - if output == "" { - return "[<NO ROLES>]" - } - - return output[:len(output)-1] -} - func init() { initializePermissions() initializeDefaultRoles() diff --git a/model/channel.go b/model/channel.go index ce812be3d..df68202d6 100644 --- a/model/channel.go +++ b/model/channel.go @@ -86,12 +86,7 @@ func (o *Channel) Etag() string { return Etag(o.Id, o.UpdateAt) } -func (o *Channel) StatsEtag() string { - return Etag(o.Id, o.ExtraUpdateAt) -} - func (o *Channel) IsValid() *AppError { - if len(o.Id) != 26 { return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest) } diff --git a/model/client4.go b/model/client4.go index 4772d38b3..9e552d046 100644 --- a/model/client4.go +++ b/model/client4.go @@ -198,6 +198,10 @@ func (c *Client4) GetTestEmailRoute() string { return fmt.Sprintf("/email/test") } +func (c *Client4) GetTestS3Route() string { + return fmt.Sprintf("/file/s3_test") +} + func (c *Client4) GetDatabaseRoute() string { return fmt.Sprintf("/database") } @@ -691,7 +695,7 @@ func (c *Client4) GetUsersNotInTeam(teamId string, page int, perPage int, etag s } } -// GetUsersInChannel returns a page of users on a team. Page counting starts at 0. +// GetUsersInChannel returns a page of users in a channel. Page counting starts at 0. func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, etag string) ([]*User, *Response) { query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage) if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil { @@ -702,7 +706,18 @@ func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, eta } } -// GetUsersNotInChannel returns a page of users on a team. Page counting starts at 0. +// GetUsersInChannelStatus returns a page of users in a channel. Page counting starts at 0. Sorted by Status +func (c *Client4) GetUsersInChannelByStatus(channelId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v&sort=status", channelId, page, perPage) + if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) + } +} + +// GetUsersNotInChannel returns a page of users not in a channel. Page counting starts at 0. func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPage int, etag string) ([]*User, *Response) { query := fmt.Sprintf("?in_team=%v¬_in_channel=%v&page=%v&per_page=%v", teamId, channelId, page, perPage) if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil { @@ -2092,6 +2107,16 @@ func (c *Client4) TestEmail() (bool, *Response) { } } +// TestS3Connection will attempt to connect to the AWS S3. +func (c *Client4) TestS3Connection(config *Config) (bool, *Response) { + if r, err := c.DoApiPost(c.GetTestS3Route(), config.ToJson()); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // GetConfig will retrieve the server config with some sanitized items. func (c *Client4) GetConfig() (*Config, *Response) { if r, err := c.DoApiGet(c.GetConfigRoute(), ""); err != nil { @@ -3304,3 +3329,56 @@ func (c *Client4) DeactivatePlugin(id string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } } + +// SetTeamIcon sets team icon of the team +func (c *Client4) SetTeamIcon(teamId string, data []byte) (bool, *Response) { + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if part, err := writer.CreateFormFile("image", "teamIcon.png"); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err := writer.Close(); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetTeamRoute(teamId)+"/image", bytes.NewReader(body.Bytes())) + rq.Header.Set("Content-Type", writer.FormDataContentType()) + rq.Close = true + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil { + // set to http.StatusForbidden(403) + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetTeamRoute(teamId)+"/image", "model.client.connecting.app_error", nil, err.Error(), 403)} + } else { + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } else { + return CheckStatusOK(rp), BuildResponse(rp) + } + } +} + +// GetTeamIcon gets the team icon of the team +func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response) { + if r, err := c.DoApiGet(c.GetTeamRoute(teamId)+"/image", etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + + if data, err := ioutil.ReadAll(r.Body); err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, err.Error(), r.StatusCode)) + } else { + return data, BuildResponse(r) + } + } +} diff --git a/model/cluster_info.go b/model/cluster_info.go index a8d63ec32..46a3487a9 100644 --- a/model/cluster_info.go +++ b/model/cluster_info.go @@ -6,7 +6,6 @@ package model import ( "encoding/json" "io" - "strings" ) type ClusterInfo struct { @@ -22,11 +21,6 @@ func (me *ClusterInfo) ToJson() string { return string(b) } -func (me *ClusterInfo) Copy() *ClusterInfo { - json := me.ToJson() - return ClusterInfoFromJson(strings.NewReader(json)) -} - func ClusterInfoFromJson(data io.Reader) *ClusterInfo { var me *ClusterInfo json.NewDecoder(data).Decode(&me) diff --git a/model/compliance_post.go b/model/compliance_post.go index 3751c5862..75e8de1f1 100644 --- a/model/compliance_post.go +++ b/model/compliance_post.go @@ -17,6 +17,7 @@ type CompliancePost struct { // From Channel ChannelName string ChannelDisplayName string + ChannelType string // From User UserUsername string @@ -45,6 +46,7 @@ func CompliancePostHeader() []string { "ChannelName", "ChannelDisplayName", + "ChannelType", "UserUsername", "UserEmail", @@ -92,6 +94,7 @@ func (me *CompliancePost) Row() []string { cleanComplianceStrings(me.ChannelName), cleanComplianceStrings(me.ChannelDisplayName), + cleanComplianceStrings(me.ChannelType), cleanComplianceStrings(me.UserUsername), cleanComplianceStrings(me.UserEmail), diff --git a/model/config.go b/model/config.go index 1b916fe13..98e331f10 100644 --- a/model/config.go +++ b/model/config.go @@ -35,10 +35,6 @@ const ( SERVICE_GOOGLE = "google" SERVICE_OFFICE365 = "office365" - WEBSERVER_MODE_REGULAR = "regular" - WEBSERVER_MODE_GZIP = "gzip" - WEBSERVER_MODE_DISABLED = "disabled" - GENERIC_NO_CHANNEL_NOTIFICATION = "generic_no_channel" GENERIC_NOTIFICATION = "generic" FULL_NOTIFICATION = "full" @@ -99,15 +95,12 @@ const ( EMAIL_SETTINGS_DEFAULT_FEEDBACK_ORGANIZATION = "" - SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK = "https://about.mattermost.com/default-terms/" - SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK = "https://about.mattermost.com/default-privacy-policy/" - SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK = "https://about.mattermost.com/default-about/" - SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/" - SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/" - SUPPORT_SETTINGS_DEFAULT_ADMINISTRATORS_GUIDE_LINK = "https://about.mattermost.com/administrators-guide/" - SUPPORT_SETTINGS_DEFAULT_TROUBLESHOOTING_FORUM_LINK = "https://about.mattermost.com/troubleshooting-forum/" - SUPPORT_SETTINGS_DEFAULT_COMMERCIAL_SUPPORT_LINK = "https://about.mattermost.com/commercial-support/" - SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com" + SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK = "https://about.mattermost.com/default-terms/" + SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK = "https://about.mattermost.com/default-privacy-policy/" + SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK = "https://about.mattermost.com/default-about/" + SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/" + SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/" + SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com" LDAP_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = "" LDAP_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = "" @@ -161,6 +154,8 @@ const ( COMPLIANCE_EXPORT_TYPE_ACTIANCE = "actiance" COMPLIANCE_EXPORT_TYPE_GLOBALRELAY = "globalrelay" + GLOBALRELAY_CUSTOMER_TYPE_A9 = "A9" + GLOBALRELAY_CUSTOMER_TYPE_A10 = "A10" ) type ServiceSettings struct { @@ -1634,6 +1629,28 @@ func (s *PluginSettings) SetDefaults() { } } +type GlobalRelayMessageExportSettings struct { + CustomerType *string // must be either A9 or A10, dictates SMTP server url + SmtpUsername *string + SmtpPassword *string + EmailAddress *string // the address to send messages to +} + +func (s *GlobalRelayMessageExportSettings) SetDefaults() { + if s.CustomerType == nil { + s.CustomerType = NewString(GLOBALRELAY_CUSTOMER_TYPE_A9) + } + if s.SmtpUsername == nil { + s.SmtpUsername = NewString("") + } + if s.SmtpPassword == nil { + s.SmtpPassword = NewString("") + } + if s.EmailAddress == nil { + s.EmailAddress = NewString("") + } +} + type MessageExportSettings struct { EnableExport *bool ExportFormat *string @@ -1642,7 +1659,7 @@ type MessageExportSettings struct { BatchSize *int // formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format - GlobalRelayEmailAddress *string + GlobalRelaySettings *GlobalRelayMessageExportSettings } func (s *MessageExportSettings) SetDefaults() { @@ -1673,6 +1690,11 @@ func (s *MessageExportSettings) SetDefaults() { if s.BatchSize == nil { s.BatchSize = NewInt(10000) } + + if s.GlobalRelaySettings == nil { + s.GlobalRelaySettings = &GlobalRelayMessageExportSettings{} + s.GlobalRelaySettings.SetDefaults() + } } type ConfigFunc func() *Config @@ -2206,10 +2228,18 @@ func (mes *MessageExportSettings) isValid(fs FileSettings) *AppError { } if *mes.ExportFormat == COMPLIANCE_EXPORT_TYPE_GLOBALRELAY { - // validating email addresses is hard - just make sure it contains an '@' sign - // see https://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address - if mes.GlobalRelayEmailAddress == nil || !strings.Contains(*mes.GlobalRelayEmailAddress, "@") { - return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay_email_address.app_error", nil, "", http.StatusBadRequest) + if mes.GlobalRelaySettings == nil { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.config_missing.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.CustomerType == nil || (*mes.GlobalRelaySettings.CustomerType != GLOBALRELAY_CUSTOMER_TYPE_A9 && *mes.GlobalRelaySettings.CustomerType != GLOBALRELAY_CUSTOMER_TYPE_A10) { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.customer_type.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.EmailAddress == nil || !strings.Contains(*mes.GlobalRelaySettings.EmailAddress, "@") { + // validating email addresses is hard - just make sure it contains an '@' sign + // see https://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.email_address.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.SmtpUsername == nil || *mes.GlobalRelaySettings.SmtpUsername == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_username.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.SmtpPassword == nil || *mes.GlobalRelaySettings.SmtpPassword == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_password.app_error", nil, "", http.StatusBadRequest) } } } diff --git a/model/config_test.go b/model/config_test.go index 919f73fd7..1f917af27 100644 --- a/model/config_test.go +++ b/model/config_test.go @@ -183,21 +183,123 @@ func TestMessageExportSettingsIsValidActiance(t *testing.T) { require.Nil(t, mes.isValid(*fs)) } -func TestMessageExportSettingsIsValidGlobalRelay(t *testing.T) { +func TestMessageExportSettingsIsValidGlobalRelaySettingsMissing(t *testing.T) { fs := &FileSettings{ DriverName: NewString("foo"), // bypass file location check } mes := &MessageExportSettings{ - EnableExport: NewBool(true), - ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY), - ExportFromTimestamp: NewInt64(0), - DailyRunTime: NewString("15:04"), - BatchSize: NewInt(100), - GlobalRelayEmailAddress: NewString("test@mattermost.com"), + EnableExport: NewBool(true), + ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY), + ExportFromTimestamp: NewInt64(0), + DailyRunTime: NewString("15:04"), + BatchSize: NewInt(100), } - // should pass because everything is valid - require.Nil(t, mes.isValid(*fs)) + // should fail because globalrelay settings are missing + require.Error(t, mes.isValid(*fs)) +} + +func TestMessageExportSettingsIsValidGlobalRelaySettingsInvalidCustomerType(t *testing.T) { + fs := &FileSettings{ + DriverName: NewString("foo"), // bypass file location check + } + mes := &MessageExportSettings{ + EnableExport: NewBool(true), + ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY), + ExportFromTimestamp: NewInt64(0), + DailyRunTime: NewString("15:04"), + BatchSize: NewInt(100), + GlobalRelaySettings: &GlobalRelayMessageExportSettings{ + CustomerType: NewString("Invalid"), + EmailAddress: NewString("valid@mattermost.com"), + SmtpUsername: NewString("SomeUsername"), + SmtpPassword: NewString("SomePassword"), + }, + } + + // should fail because customer type is invalid + require.Error(t, mes.isValid(*fs)) +} + +// func TestMessageExportSettingsIsValidGlobalRelaySettingsInvalidEmailAddress(t *testing.T) { +func TestMessageExportSettingsGlobalRelaySettings(t *testing.T) { + fs := &FileSettings{ + DriverName: NewString("foo"), // bypass file location check + } + tests := []struct { + name string + value *GlobalRelayMessageExportSettings + success bool + }{ + { + "Invalid email address", + &GlobalRelayMessageExportSettings{ + CustomerType: NewString(GLOBALRELAY_CUSTOMER_TYPE_A9), + EmailAddress: NewString("invalidEmailAddress"), + SmtpUsername: NewString("SomeUsername"), + SmtpPassword: NewString("SomePassword"), + }, + false, + }, + { + "Missing smtp username", + &GlobalRelayMessageExportSettings{ + CustomerType: NewString(GLOBALRELAY_CUSTOMER_TYPE_A10), + EmailAddress: NewString("valid@mattermost.com"), + SmtpPassword: NewString("SomePassword"), + }, + false, + }, + { + "Invalid smtp username", + &GlobalRelayMessageExportSettings{ + CustomerType: NewString(GLOBALRELAY_CUSTOMER_TYPE_A10), + EmailAddress: NewString("valid@mattermost.com"), + SmtpUsername: NewString(""), + SmtpPassword: NewString("SomePassword"), + }, + false, + }, + { + "Invalid smtp password", + &GlobalRelayMessageExportSettings{ + CustomerType: NewString(GLOBALRELAY_CUSTOMER_TYPE_A10), + EmailAddress: NewString("valid@mattermost.com"), + SmtpUsername: NewString("SomeUsername"), + SmtpPassword: NewString(""), + }, + false, + }, + { + "Valid data", + &GlobalRelayMessageExportSettings{ + CustomerType: NewString(GLOBALRELAY_CUSTOMER_TYPE_A9), + EmailAddress: NewString("valid@mattermost.com"), + SmtpUsername: NewString("SomeUsername"), + SmtpPassword: NewString("SomePassword"), + }, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mes := &MessageExportSettings{ + EnableExport: NewBool(true), + ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY), + ExportFromTimestamp: NewInt64(0), + DailyRunTime: NewString("15:04"), + BatchSize: NewInt(100), + GlobalRelaySettings: tt.value, + } + + if tt.success { + require.Nil(t, mes.isValid(*fs)) + } else { + require.Error(t, mes.isValid(*fs)) + } + }) + } } func TestMessageExportSetDefaults(t *testing.T) { diff --git a/model/emoji.go b/model/emoji.go index a1703abb1..47d170bb3 100644 --- a/model/emoji.go +++ b/model/emoji.go @@ -56,10 +56,6 @@ func (emoji *Emoji) PreSave() { emoji.UpdateAt = emoji.CreateAt } -func (emoji *Emoji) PreUpdate() { - emoji.UpdateAt = GetMillis() -} - func (emoji *Emoji) ToJson() string { b, _ := json.Marshal(emoji) return string(b) diff --git a/model/ldap.go b/model/ldap.go index 1453a4add..9051c5a30 100644 --- a/model/ldap.go +++ b/model/ldap.go @@ -5,5 +5,4 @@ package model const ( USER_AUTH_SERVICE_LDAP = "ldap" - LDAP_SYNC_TASK_NAME = "LDAP Syncronization" ) diff --git a/model/manifest.go b/model/manifest.go index 2b415dc62..32d4341cd 100644 --- a/model/manifest.go +++ b/model/manifest.go @@ -13,15 +13,6 @@ import ( "gopkg.in/yaml.v2" ) -const ( - PLUGIN_CONFIG_TYPE_TEXT = "text" - PLUGIN_CONFIG_TYPE_BOOL = "bool" - PLUGIN_CONFIG_TYPE_RADIO = "radio" - PLUGIN_CONFIG_TYPE_DROPDOWN = "dropdown" - PLUGIN_CONFIG_TYPE_GENERATED = "generated" - PLUGIN_CONFIG_TYPE_USERNAME = "username" -) - type PluginOption struct { // The display name for the option. DisplayName string `json:"display_name" yaml:"display_name"` diff --git a/model/manifest_test.go b/model/manifest_test.go index 3fdc13ec4..b63e388bc 100644 --- a/model/manifest_test.go +++ b/model/manifest_test.go @@ -74,7 +74,7 @@ func TestManifestUnmarshal(t *testing.T) { &PluginSetting{ Key: "thesetting", DisplayName: "thedisplayname", - Type: PLUGIN_CONFIG_TYPE_DROPDOWN, + Type: "dropdown", HelpText: "thehelptext", RegenerateHelpText: "theregeneratehelptext", Placeholder: "theplaceholder", @@ -181,7 +181,7 @@ func TestManifestJson(t *testing.T) { &PluginSetting{ Key: "thesetting", DisplayName: "thedisplayname", - Type: PLUGIN_CONFIG_TYPE_DROPDOWN, + Type: "dropdown", HelpText: "thehelptext", RegenerateHelpText: "theregeneratehelptext", Placeholder: "theplaceholder", @@ -246,7 +246,7 @@ func TestManifestClientManifest(t *testing.T) { &PluginSetting{ Key: "thesetting", DisplayName: "thedisplayname", - Type: PLUGIN_CONFIG_TYPE_DROPDOWN, + Type: "dropdown", HelpText: "thehelptext", RegenerateHelpText: "theregeneratehelptext", Placeholder: "theplaceholder", diff --git a/model/message_export.go b/model/message_export.go index 22641deee..6efb8c6a4 100644 --- a/model/message_export.go +++ b/model/message_export.go @@ -6,6 +6,7 @@ package model type MessageExport struct { ChannelId *string ChannelDisplayName *string + ChannelType *string UserId *string UserEmail *string diff --git a/model/oauth.go b/model/oauth.go index 70e8a3f26..c92b1ec41 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -141,17 +141,6 @@ func OAuthAppFromJson(data io.Reader) *OAuthApp { return app } -func OAuthAppMapToJson(a map[string]*OAuthApp) string { - b, _ := json.Marshal(a) - return string(b) -} - -func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp { - var apps map[string]*OAuthApp - json.NewDecoder(data).Decode(&apps) - return apps -} - func OAuthAppListToJson(l []*OAuthApp) string { b, _ := json.Marshal(l) return string(b) diff --git a/model/saml.go b/model/saml.go index e74750156..528ac45cc 100644 --- a/model/saml.go +++ b/model/saml.go @@ -11,9 +11,6 @@ import ( const ( USER_AUTH_SERVICE_SAML = "saml" USER_AUTH_SERVICE_SAML_TEXT = "With SAML" - SAML_IDP_CERTIFICATE = 1 - SAML_PRIVATE_KEY = 2 - SAML_PUBLIC_CERT = 3 ) type SamlAuthRequest struct { diff --git a/model/search_params.go b/model/search_params.go index 1692b3aaf..481671ab5 100644 --- a/model/search_params.go +++ b/model/search_params.go @@ -4,7 +4,6 @@ package model import ( - "encoding/json" "regexp" "strings" ) @@ -20,11 +19,6 @@ type SearchParams struct { OrTerms bool } -func (o *SearchParams) ToJson() string { - b, _ := json.Marshal(o) - return string(b) -} - var searchFlags = [...]string{"from", "channel", "in"} func splitWords(text string) []string { diff --git a/model/team.go b/model/team.go index 5b6eb1fa0..7968c9d48 100644 --- a/model/team.go +++ b/model/team.go @@ -26,19 +26,20 @@ const ( ) type Team struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - DisplayName string `json:"display_name"` - Name string `json:"name"` - Description string `json:"description"` - Email string `json:"email"` - Type string `json:"type"` - CompanyName string `json:"company_name"` - AllowedDomains string `json:"allowed_domains"` - InviteId string `json:"invite_id"` - AllowOpenInvite bool `json:"allow_open_invite"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Description string `json:"description"` + Email string `json:"email"` + Type string `json:"type"` + CompanyName string `json:"company_name"` + AllowedDomains string `json:"allowed_domains"` + InviteId string `json:"invite_id"` + AllowOpenInvite bool `json:"allow_open_invite"` + LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"` } type TeamPatch struct { @@ -242,15 +243,6 @@ func (o *Team) Sanitize() { o.AllowedDomains = "" } -func (o *Team) SanitizeForNotLoggedIn() { - o.Email = "" - o.AllowedDomains = "" - o.CompanyName = "" - if !o.AllowOpenInvite { - o.InviteId = "" - } -} - func (t *Team) Patch(patch *TeamPatch) { if patch.DisplayName != nil { t.DisplayName = *patch.DisplayName diff --git a/model/user.go b/model/user.go index 1e1d49f7d..f64275c83 100644 --- a/model/user.go +++ b/model/user.go @@ -373,12 +373,6 @@ func (u *User) MakeNonNil() { } } -func (u *User) AddProp(key string, value string) { - u.MakeNonNil() - - u.Props[key] = value -} - func (u *User) AddNotifyProp(key string, value string) { u.MakeNonNil() diff --git a/model/utils.go b/model/utils.go index 331a1aaaa..72369852b 100644 --- a/model/utils.go +++ b/model/utils.go @@ -394,9 +394,6 @@ func ClearMentionTags(post string) string { return post } -var UrlRegex = regexp.MustCompile(`^((?:[a-z]+:\/\/)?(?:(?:[a-z0-9\-]+\.)+(?:[a-z]{2}|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|local|internal))(:[0-9]{1,5})?(?:\/[a-z0-9_\-\.~]+)*(\/([a-z0-9_\-\.]*)(?:\?[a-z0-9+_~\-\.%=&]*)?)?(?:#[a-zA-Z0-9!$&'()*+.=-_~:@/?]*)?)(?:\s+|$)$`) -var PartialUrlRegex = regexp.MustCompile(`/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/((?:[A-Za-z0-9]{26})?.+(?:\.[A-Za-z0-9]{3,})?)`) - func IsValidHttpUrl(rawUrl string) bool { if strings.Index(rawUrl, "http://") != 0 && strings.Index(rawUrl, "https://") != 0 { return false @@ -409,18 +406,6 @@ func IsValidHttpUrl(rawUrl string) bool { return true } -func IsValidHttpsUrl(rawUrl string) bool { - if strings.Index(rawUrl, "https://") != 0 { - return false - } - - if _, err := url.ParseRequestURI(rawUrl); err != nil { - return false - } - - return true -} - func IsValidTurnOrStunServer(rawUri string) bool { if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 { return false diff --git a/model/version.go b/model/version.go index 3e75478d3..e4e0af491 100644 --- a/model/version.go +++ b/model/version.go @@ -107,10 +107,6 @@ func GetPreviousVersion(version string) string { return "" } -func IsOfficalBuild() bool { - return BuildNumber != "_BUILD_NUMBER_" -} - func IsCurrentVersion(versionToCheck string) bool { currentMajor, currentMinor, _ := SplitVersion(CurrentVersion) toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) diff --git a/model/websocket_message.go b/model/websocket_message.go index 8d1abecfa..76326ee3f 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -44,6 +44,8 @@ const ( WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed" WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE + WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed" + WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed" ) type WebSocketMessage interface { diff --git a/scripts/prereq-check.sh b/scripts/prereq-check.sh index 1c9ae8405..6f2954273 100755 --- a/scripts/prereq-check.sh +++ b/scripts/prereq-check.sh @@ -2,7 +2,7 @@ check_version() { local version=$1 check=$2 - local winner=$(echo -e "$version\n$check" | sed '/^$/d' | sort -nr | head -1) + local winner=$(echo -e "$version\n$check" | sed '/^$/d' | sort -t. -s -k 1,1nr -k 2,2nr -k 3,3nr -k 4,4nr | head -1) [[ "$winner" = "$version" ]] && return 0 return 1 } @@ -46,4 +46,4 @@ DOCKERVERSION=$(docker version --format '{{.Server.Version}}' | sed 's/[a-z-]//g check_prereq 'node' $REQUIREDNODEVERSION $NODEVERSION check_prereq 'npm' $REQUIREDNPMVERSION $NPMVERSION check_prereq 'go' $REQUIREDGOVERSION $GOVERSION -check_prereq 'docker' $REQUIREDDOCKERVERSION $DOCKERVERSION
\ No newline at end of file +check_prereq 'docker' $REQUIREDDOCKERVERSION $DOCKERVERSION diff --git a/store/layered_store_supplier.go b/store/layered_store_supplier.go index 841b75a32..d5e654019 100644 --- a/store/layered_store_supplier.go +++ b/store/layered_store_supplier.go @@ -6,8 +6,6 @@ package store import "github.com/mattermost/mattermost-server/model" import "context" -type ResultHandler func(*StoreResult) - type LayeredStoreSupplierResult struct { StoreResult } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 75a615aee..e7a157192 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -43,12 +43,20 @@ var allChannelMembersNotifyPropsForChannelCache = utils.NewLru(ALL_CHANNEL_MEMBE var channelCache = utils.NewLru(model.CHANNEL_CACHE_SIZE) var channelByNameCache = utils.NewLru(model.CHANNEL_CACHE_SIZE) -func ClearChannelCaches() { +func (s SqlChannelStore) ClearCaches() { channelMemberCountsCache.Purge() allChannelMembersForUserCache.Purge() allChannelMembersNotifyPropsForChannelCache.Purge() channelCache.Purge() channelByNameCache.Purge() + + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Purge") + s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Purge") + s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Purge") + s.metrics.IncrementMemCacheInvalidationCounter("Channel - Purge") + s.metrics.IncrementMemCacheInvalidationCounter("Channel By Name - Purge") + } } func NewSqlChannelStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.ChannelStore { @@ -308,12 +316,18 @@ func (s SqlChannelStore) GetChannelUnread(channelId, userId string) store.StoreC }) } -func (us SqlChannelStore) InvalidateChannel(id string) { +func (s SqlChannelStore) InvalidateChannel(id string) { channelCache.Remove(id) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Channel - Remove by ChannelId") + } } -func (us SqlChannelStore) InvalidateChannelByName(teamId, name string) { +func (s SqlChannelStore) InvalidateChannelByName(teamId, name string) { channelByNameCache.Remove(teamId + name) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Channel by Name - Remove by TeamId and Name") + } } func (s SqlChannelStore) Get(id string, allowFromCache bool) store.StoreChannel { @@ -814,14 +828,17 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) store.StoreC }) } -func (us SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) { +func (s SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) { allChannelMembersForUserCache.Remove(userId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Remove by UserId") + } } -func (us SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { +func (s SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { if cacheItem, ok := allChannelMembersForUserCache.Get(userId); ok { - if us.metrics != nil { - us.metrics.IncrementMemCacheHitCounter("All Channel Members for User") + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("All Channel Members for User") } ids := cacheItem.(map[string]string) if _, ok := ids[channelId]; ok { @@ -830,12 +847,12 @@ func (us SqlChannelStore) IsUserInChannelUseCache(userId string, channelId strin return false } } else { - if us.metrics != nil { - us.metrics.IncrementMemCacheMissCounter("All Channel Members for User") + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("All Channel Members for User") } } - if result := <-us.GetAllChannelMembersForUser(userId, true); result.Err != nil { + if result := <-s.GetAllChannelMembersForUser(userId, true); result.Err != nil { l4g.Error("SqlChannelStore.IsUserInChannelUseCache: " + result.Err.Error()) return false } else { @@ -915,8 +932,11 @@ func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCac }) } -func (us SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) { +func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) { allChannelMembersNotifyPropsForChannelCache.Remove(channelId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Remove by ChannelId") + } } type allChannelMemberNotifyProps struct { @@ -946,9 +966,9 @@ func (s SqlChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId str var data []allChannelMemberNotifyProps _, err := s.GetReplica().Select(&data, ` - SELECT ChannelMembers.UserId, ChannelMembers.NotifyProps - FROM Channels, ChannelMembers - WHERE Channels.Id = ChannelMembers.ChannelId AND ChannelMembers.ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId}) + SELECT UserId, NotifyProps + FROM ChannelMembers + WHERE ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId}) if err != nil { result.Err = model.NewAppError("SqlChannelStore.GetAllChannelMembersPropsForChannel", "store.sql_channel.get_members.app_error", nil, "channelId="+channelId+", err="+err.Error(), http.StatusInternalServerError) @@ -966,8 +986,11 @@ func (s SqlChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId str }) } -func (us SqlChannelStore) InvalidateMemberCount(channelId string) { +func (s SqlChannelStore) InvalidateMemberCount(channelId string) { channelMemberCountsCache.Remove(channelId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Remove by ChannelId") + } } func (s SqlChannelStore) GetMemberCountFromCache(channelId string) int64 { @@ -1225,19 +1248,6 @@ func (s SqlChannelStore) AnalyticsDeletedTypeCount(teamId string, channelType st }) } -func (s SqlChannelStore) ExtraUpdateByUser(userId string, time int64) store.StoreChannel { - return store.Do(func(result *store.StoreResult) { - _, err := s.GetMaster().Exec( - `UPDATE Channels SET ExtraUpdateAt = :Time - WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId);`, - map[string]interface{}{"UserId": userId, "Time": time}) - - if err != nil { - result.Err = model.NewAppError("SqlChannelStore.extraUpdated", "store.sql_channel.extra_updated.app_error", nil, "user_id="+userId+", "+err.Error(), http.StatusInternalServerError) - } - }) -} - func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { members := &model.ChannelMembers{} diff --git a/store/sqlstore/compliance_store.go b/store/sqlstore/compliance_store.go index 03d92d5e1..c3c75581e 100644 --- a/store/sqlstore/compliance_store.go +++ b/store/sqlstore/compliance_store.go @@ -138,6 +138,7 @@ func (s SqlComplianceStore) ComplianceExport(job *model.Compliance) store.StoreC Teams.DisplayName AS TeamDisplayName, Channels.Name AS ChannelName, Channels.DisplayName AS ChannelDisplayName, + Channels.Type AS ChannelType, Users.Username AS UserUsername, Users.Email AS UserEmail, Users.Nickname AS UserNickname, @@ -172,6 +173,7 @@ func (s SqlComplianceStore) ComplianceExport(job *model.Compliance) store.StoreC 'Direct Messages' AS TeamDisplayName, Channels.Name AS ChannelName, Channels.DisplayName AS ChannelDisplayName, + Channels.Type AS ChannelType, Users.Username AS UserUsername, Users.Email AS UserEmail, Users.Nickname AS UserNickname, @@ -223,7 +225,12 @@ func (s SqlComplianceStore) MessageExport(after int64, limit int) store.StoreCha Posts.Type AS PostType, Posts.FileIds AS PostFileIds, Channels.Id AS ChannelId, - Channels.DisplayName AS ChannelDisplayName, + CASE + WHEN Channels.Type = 'D' THEN 'Direct Message' + WHEN Channels.Type = 'G' THEN 'Group Message' + ELSE Channels.DisplayName + END AS ChannelDisplayName, + Channels.Type AS ChannelType, Users.Id AS UserId, Users.Email AS UserEmail, Users.Username diff --git a/store/sqlstore/file_info_store.go b/store/sqlstore/file_info_store.go index 1d0767d1e..7559640c8 100644 --- a/store/sqlstore/file_info_store.go +++ b/store/sqlstore/file_info_store.go @@ -25,8 +25,11 @@ const ( var fileInfoCache *utils.Cache = utils.NewLru(FILE_INFO_CACHE_SIZE) -func ClearFileCaches() { +func (fs SqlFileInfoStore) ClearCaches() { fileInfoCache.Purge() + if fs.metrics != nil { + fs.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Purge") + } } func NewSqlFileInfoStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.FileInfoStore { @@ -118,6 +121,9 @@ func (fs SqlFileInfoStore) GetByPath(path string) store.StoreChannel { func (fs SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string) { fileInfoCache.Remove(postId) + if fs.metrics != nil { + fs.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Remove by PostId") + } } func (fs SqlFileInfoStore) GetForPost(postId string, readFromMaster bool, allowFromCache bool) store.StoreChannel { diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 25c3c4913..92ee28ffa 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -35,9 +35,14 @@ const ( var lastPostTimeCache = utils.NewLru(LAST_POST_TIME_CACHE_SIZE) var lastPostsCache = utils.NewLru(LAST_POSTS_CACHE_SIZE) -func ClearPostCaches() { +func (s SqlPostStore) ClearCaches() { lastPostTimeCache.Purge() lastPostsCache.Purge() + + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Purge") + s.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Purge") + } } func NewSqlPostStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.PostStore { @@ -326,6 +331,11 @@ func (s SqlPostStore) InvalidateLastPostTimeCache(channelId string) { // Keys are "{channelid}{limit}" and caching only occurs on limits of 30 and 60 lastPostsCache.Remove(channelId + "30") lastPostsCache.Remove(channelId + "60") + + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Remove by Channel Id") + s.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Remove by Channel Id") + } } func (s SqlPostStore) GetEtag(channelId string, allowFromCache bool) store.StoreChannel { diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index cddfb7c1a..6528b8e4c 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -99,6 +99,7 @@ func (s SqlTeamStore) Update(team *model.Team) store.StoreChannel { team.CreateAt = oldTeam.CreateAt team.UpdateAt = model.GetMillis() team.Name = oldTeam.Name + team.LastTeamIconUpdate = oldTeam.LastTeamIconUpdate if count, err := s.GetMaster().Update(team); err != nil { result.Err = model.NewAppError("SqlTeamStore.Update", "store.sql_team.update.updating.app_error", nil, "id="+team.Id+", "+err.Error(), http.StatusInternalServerError) @@ -559,3 +560,13 @@ func (s SqlTeamStore) RemoveAllMembersByUser(userId string) store.StoreChannel { } }) } + +func (us SqlTeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + if _, err := us.GetMaster().Exec("UPDATE Teams SET LastTeamIconUpdate = :Time, UpdateAt = :Time WHERE Id = :teamId", map[string]interface{}{"Time": curTime, "teamId": teamId}); err != nil { + result.Err = model.NewAppError("SqlTeamStore.UpdateLastTeamIconUpdate", "store.sql_team.update_last_team_icon_update.app_error", nil, "team_id="+teamId, http.StatusInternalServerError) + } else { + result.Data = teamId + } + }) +} diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 1e704ec01..58de85c7f 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -15,6 +15,7 @@ import ( ) const ( + VERSION_4_9_0 = "4.9.0" VERSION_4_8_0 = "4.8.0" VERSION_4_7_1 = "4.7.1" VERSION_4_7_0 = "4.7.0" @@ -68,6 +69,7 @@ func UpgradeDatabase(sqlStore SqlStore) { UpgradeDatabaseToVersion47(sqlStore) UpgradeDatabaseToVersion471(sqlStore) UpgradeDatabaseToVersion48(sqlStore) + UpgradeDatabaseToVersion49(sqlStore) // If the SchemaVersion is empty this this is the first time it has ran // so lets set it to the current version. @@ -369,3 +371,11 @@ func UpgradeDatabaseToVersion48(sqlStore SqlStore) { saveSchemaVersion(sqlStore, VERSION_4_8_0) } } + +func UpgradeDatabaseToVersion49(sqlStore SqlStore) { + //TODO: Uncomment the following condition when version 4.9.0 is released + //if shouldPerformUpgrade(sqlStore, VERSION_4_8_0, VERSION_4_9_0) { + sqlStore.CreateColumnIfNotExists("Teams", "LastTeamIconUpdate", "bigint", "bigint", "0") + // saveSchemaVersion(sqlStore, VERSION_4_9_0) + //} +} diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index d67a45704..5e84af930 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -38,13 +38,22 @@ type SqlUserStore struct { var profilesInChannelCache *utils.Cache = utils.NewLru(PROFILES_IN_CHANNEL_CACHE_SIZE) var profileByIdsCache *utils.Cache = utils.NewLru(PROFILE_BY_IDS_CACHE_SIZE) -func ClearUserCaches() { +func (us SqlUserStore) ClearCaches() { profilesInChannelCache.Purge() profileByIdsCache.Purge() + + if us.metrics != nil { + us.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Purge") + us.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Purge") + } } func (us SqlUserStore) InvalidatProfileCacheForUser(userId string) { profileByIdsCache.Remove(userId) + + if us.metrics != nil { + us.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Remove") + } } func NewSqlUserStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.UserStore { @@ -384,6 +393,9 @@ func (us SqlUserStore) InvalidateProfilesInChannelCacheByUser(userId string) { userMap := cacheItem.(map[string]*model.User) if _, userInCache := userMap[userId]; userInCache { profilesInChannelCache.Remove(key) + if us.metrics != nil { + us.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by User") + } } } } @@ -391,13 +403,27 @@ func (us SqlUserStore) InvalidateProfilesInChannelCacheByUser(userId string) { func (us SqlUserStore) InvalidateProfilesInChannelCache(channelId string) { profilesInChannelCache.Remove(channelId) + if us.metrics != nil { + us.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by Channel") + } } func (us SqlUserStore) GetProfilesInChannel(channelId string, offset int, limit int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { var users []*model.User - query := "SELECT Users.* FROM Users, ChannelMembers WHERE ChannelMembers.ChannelId = :ChannelId AND Users.Id = ChannelMembers.UserId ORDER BY Users.Username ASC LIMIT :Limit OFFSET :Offset" + query := ` + SELECT + Users.* + FROM + Users, ChannelMembers + WHERE + ChannelMembers.ChannelId = :ChannelId + AND Users.Id = ChannelMembers.UserId + ORDER BY + Users.Username ASC + LIMIT :Limit OFFSET :Offset + ` if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil { result.Err = model.NewAppError("SqlUserStore.GetProfilesInChannel", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -412,6 +438,42 @@ func (us SqlUserStore) GetProfilesInChannel(channelId string, offset int, limit }) } +func (us SqlUserStore) GetProfilesInChannelByStatus(channelId string, offset int, limit int) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var users []*model.User + + query := ` + SELECT + Users.* + FROM Users + INNER JOIN ChannelMembers ON Users.Id = ChannelMembers.UserId + LEFT JOIN Status ON Users.Id = Status.UserId + WHERE + ChannelMembers.ChannelId = :ChannelId + ORDER BY + CASE Status + WHEN 'online' THEN 1 + WHEN 'away' THEN 2 + WHEN 'dnd' THEN 3 + ELSE 4 + END, + Users.Username ASC + LIMIT :Limit OFFSET :Offset + ` + + if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil { + result.Err = model.NewAppError("SqlUserStore.GetProfilesInChannelByStatus", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) + } else { + + for _, u := range users { + u.Sanitize(map[string]bool{}) + } + + result.Data = users + } + }) +} + func (us SqlUserStore) GetAllProfilesInChannel(channelId string, allowFromCache bool) store.StoreChannel { return store.Do(func(result *store.StoreResult) { if allowFromCache { diff --git a/store/sqlstore/webhook_store.go b/store/sqlstore/webhook_store.go index 8a3720fa0..45ad90e52 100644 --- a/store/sqlstore/webhook_store.go +++ b/store/sqlstore/webhook_store.go @@ -26,8 +26,12 @@ const ( var webhookCache = utils.NewLru(WEBHOOK_CACHE_SIZE) -func ClearWebhookCaches() { +func (s SqlWebhookStore) ClearCaches() { webhookCache.Purge() + + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Webhook - Purge") + } } func NewSqlWebhookStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.WebhookStore { @@ -78,6 +82,9 @@ func (s SqlWebhookStore) CreateIndexesIfNotExists() { func (s SqlWebhookStore) InvalidateWebhookCache(webhookId string) { webhookCache.Remove(webhookId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Webhook - Remove by WebhookId") + } } func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) store.StoreChannel { @@ -164,7 +171,7 @@ func (s SqlWebhookStore) PermanentDeleteIncomingByUser(userId string) store.Stor result.Err = model.NewAppError("SqlWebhookStore.DeleteIncomingByUser", "store.sql_webhooks.permanent_delete_incoming_by_user.app_error", nil, "id="+userId+", err="+err.Error(), http.StatusInternalServerError) } - ClearWebhookCaches() + s.ClearCaches() }) } @@ -175,7 +182,7 @@ func (s SqlWebhookStore) PermanentDeleteIncomingByChannel(channelId string) stor result.Err = model.NewAppError("SqlWebhookStore.DeleteIncomingByChannel", "store.sql_webhooks.permanent_delete_incoming_by_channel.app_error", nil, "id="+channelId+", err="+err.Error(), http.StatusInternalServerError) } - ClearWebhookCaches() + s.ClearCaches() }) } @@ -322,7 +329,7 @@ func (s SqlWebhookStore) PermanentDeleteOutgoingByChannel(channelId string) stor result.Err = model.NewAppError("SqlWebhookStore.DeleteOutgoingByChannel", "store.sql_webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "id="+channelId+", err="+err.Error(), http.StatusInternalServerError) } - ClearWebhookCaches() + s.ClearCaches() }) } diff --git a/store/store.go b/store/store.go index 85f215ab9..f070a45db 100644 --- a/store/store.go +++ b/store/store.go @@ -103,6 +103,7 @@ type TeamStore interface { RemoveMember(teamId string, userId string) StoreChannel RemoveAllMembersByTeam(teamId string) StoreChannel RemoveAllMembersByUser(userId string) StoreChannel + UpdateLastTeamIconUpdate(teamId string, curTime int64) StoreChannel } type ChannelStore interface { @@ -152,7 +153,6 @@ type ChannelStore interface { UpdateLastViewedAt(channelIds []string, userId string) StoreChannel IncrementMentionCount(channelId string, userId string) StoreChannel AnalyticsTypeCount(teamId string, channelType string) StoreChannel - ExtraUpdateByUser(userId string, time int64) StoreChannel GetMembersForUser(teamId string, userId string) StoreChannel AutocompleteInTeam(teamId string, term string) StoreChannel SearchInTeam(teamId string, term string) StoreChannel @@ -160,6 +160,7 @@ type ChannelStore interface { GetMembersByIds(channelId string, userIds []string) StoreChannel AnalyticsDeletedTypeCount(teamId string, channelType string) StoreChannel GetChannelUnread(channelId, userId string) StoreChannel + ClearCaches() } type ChannelMemberHistoryStore interface { @@ -189,6 +190,7 @@ type PostStore interface { AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel AnalyticsPostCountsByDay(teamId string) StoreChannel AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel + ClearCaches() InvalidateLastPostTimeCache(channelId string) GetPostsCreatedAt(channelId string, time int64) StoreChannel Overwrite(post *model.Post) StoreChannel @@ -209,9 +211,11 @@ type UserStore interface { UpdateMfaActive(userId string, active bool) StoreChannel Get(id string) StoreChannel GetAll() StoreChannel + ClearCaches() InvalidateProfilesInChannelCacheByUser(userId string) InvalidateProfilesInChannelCache(channelId string) GetProfilesInChannel(channelId string, offset int, limit int) StoreChannel + GetProfilesInChannelByStatus(channelId string, offset int, limit int) StoreChannel GetAllProfilesInChannel(channelId string, allowFromCache bool) StoreChannel GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel GetProfilesWithoutTeam(offset int, limit int) StoreChannel @@ -342,6 +346,7 @@ type WebhookStore interface { AnalyticsIncomingCount(teamId string) StoreChannel AnalyticsOutgoingCount(teamId string) StoreChannel InvalidateWebhookCache(webhook string) + ClearCaches() } type CommandStore interface { @@ -419,6 +424,7 @@ type FileInfoStore interface { DeleteForPost(postId string) StoreChannel PermanentDelete(fileId string) StoreChannel PermanentDeleteBatch(endTime int64, limit int64) StoreChannel + ClearCaches() } type ReactionStore interface { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 121b40a01..d3b69edea 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -43,7 +43,6 @@ func TestChannelStore(t *testing.T, ss store.Store) { t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) }) t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) }) t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) }) - t.Run("UpdateExtrasByUser", func(t *testing.T) { testUpdateExtrasByUser(t, ss) }) t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) @@ -1641,54 +1640,6 @@ func testGetMemberCount(t *testing.T, ss store.Store) { } } -func testUpdateExtrasByUser(t *testing.T, ss store.Store) { - teamId := model.NewId() - - c1 := model.Channel{ - TeamId: teamId, - DisplayName: "Channel1", - Name: "zz" + model.NewId() + "b", - Type: model.CHANNEL_OPEN, - } - store.Must(ss.Channel().Save(&c1, -1)) - - c2 := model.Channel{ - TeamId: teamId, - DisplayName: "Channel2", - Name: "zz" + model.NewId() + "b", - Type: model.CHANNEL_OPEN, - } - store.Must(ss.Channel().Save(&c2, -1)) - - u1 := &model.User{ - Email: model.NewId(), - DeleteAt: 0, - } - store.Must(ss.User().Save(u1)) - store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) - - m1 := model.ChannelMember{ - ChannelId: c1.Id, - UserId: u1.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - } - store.Must(ss.Channel().SaveMember(&m1)) - - u1.DeleteAt = model.GetMillis() - store.Must(ss.User().Update(u1, true)) - - if result := <-ss.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil { - t.Fatalf("failed to update extras by user: %v", result.Err) - } - - u1.DeleteAt = 0 - store.Must(ss.User().Update(u1, true)) - - if result := <-ss.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil { - t.Fatalf("failed to update extras by user: %v", result.Err) - } -} - func testChannelStoreSearchMore(t *testing.T, ss store.Store) { o1 := model.Channel{} o1.TeamId = model.NewId() diff --git a/store/storetest/compliance_store.go b/store/storetest/compliance_store.go index eb29bedc7..50a62531f 100644 --- a/store/storetest/compliance_store.go +++ b/store/storetest/compliance_store.go @@ -16,7 +16,10 @@ func TestComplianceStore(t *testing.T, ss store.Store) { t.Run("", func(t *testing.T) { testComplianceStore(t, ss) }) t.Run("ComplianceExport", func(t *testing.T) { testComplianceExport(t, ss) }) t.Run("ComplianceExportDirectMessages", func(t *testing.T) { testComplianceExportDirectMessages(t, ss) }) - t.Run("MessageExport", func(t *testing.T) { testComplianceMessageExport(t, ss) }) + t.Run("MessageExportPublicChannel", func(t *testing.T) { testMessageExportPublicChannel(t, ss) }) + t.Run("MessageExportPrivateChannel", func(t *testing.T) { testMessageExportPrivateChannel(t, ss) }) + t.Run("MessageExportDirectMessageChannel", func(t *testing.T) { testMessageExportDirectMessageChannel(t, ss) }) + t.Run("MessageExportGroupMessageChannel", func(t *testing.T) { testMessageExportGroupMessageChannel(t, ss) }) } func testComplianceStore(t *testing.T, ss store.Store) { @@ -319,7 +322,7 @@ func testComplianceExportDirectMessages(t *testing.T, ss store.Store) { } } -func testComplianceMessageExport(t *testing.T, ss store.Store) { +func testMessageExportPublicChannel(t *testing.T, ss store.Store) { // get the starting number of message export entries startTime := model.GetMillis() var numMessageExports = 0 @@ -360,15 +363,14 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) { UserId: user2.Id, }, -1)) - // need a public channel as well as a DM channel between the two users + // need a public channel channel := &model.Channel{ TeamId: team.Id, Name: model.NewId(), - DisplayName: "Channel2", + DisplayName: "Public Channel", Type: model.CHANNEL_OPEN, } channel = store.Must(ss.Channel().Save(channel, -1)).(*model.Channel) - directMessageChannel := store.Must(ss.Channel().CreateDirectChannel(user1.Id, user2.Id)).(*model.Channel) // user1 posts twice in the public channel post1 := &model.Post{ @@ -387,22 +389,114 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) { } post2 = store.Must(ss.Post().Save(post2)).(*model.Post) - // user1 also sends a DM to user2 - post3 := &model.Post{ - ChannelId: directMessageChannel.Id, + // fetch the message exports for both posts that user1 sent + messageExportMap := map[string]model.MessageExport{} + if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil { + t.Fatal(r1.Err) + } else { + messages := r1.Data.([]*model.MessageExport) + assert.Equal(t, numMessageExports+2, len(messages)) + + for _, v := range messages { + messageExportMap[*v.PostId] = *v + } + } + + // post1 was made by user1 in channel1 and team1 + assert.Equal(t, post1.Id, *messageExportMap[post1.Id].PostId) + assert.Equal(t, post1.CreateAt, *messageExportMap[post1.Id].PostCreateAt) + assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage) + assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId) + assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName) + assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId) + assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail) + assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username) + + // post2 was made by user1 in channel1 and team1 + assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId) + assert.Equal(t, post2.CreateAt, *messageExportMap[post2.Id].PostCreateAt) + assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage) + assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId) + assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName) + assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId) + assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail) + assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username) +} + +func testMessageExportPrivateChannel(t *testing.T, ss store.Store) { + // get the starting number of message export entries + startTime := model.GetMillis() + var numMessageExports = 0 + if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil { + t.Fatal(r1.Err) + } else { + messages := r1.Data.([]*model.MessageExport) + numMessageExports = len(messages) + } + + // need a team + team := &model.Team{ + DisplayName: "DisplayName", + Name: "zz" + model.NewId() + "b", + Email: model.NewId() + "@nowhere.com", + Type: model.TEAM_OPEN, + } + team = store.Must(ss.Team().Save(team)).(*model.Team) + + // and two users that are a part of that team + user1 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + } + user1 = store.Must(ss.User().Save(user1)).(*model.User) + store.Must(ss.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: user1.Id, + }, -1)) + + user2 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + } + user2 = store.Must(ss.User().Save(user2)).(*model.User) + store.Must(ss.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: user2.Id, + }, -1)) + + // need a private channel + channel := &model.Channel{ + TeamId: team.Id, + Name: model.NewId(), + DisplayName: "Private Channel", + Type: model.CHANNEL_PRIVATE, + } + channel = store.Must(ss.Channel().Save(channel, -1)).(*model.Channel) + + // user1 posts twice in the private channel + post1 := &model.Post{ + ChannelId: channel.Id, UserId: user1.Id, - CreateAt: startTime + 20, - Message: "zz" + model.NewId() + "c", + CreateAt: startTime, + Message: "zz" + model.NewId() + "a", + } + post1 = store.Must(ss.Post().Save(post1)).(*model.Post) + + post2 := &model.Post{ + ChannelId: channel.Id, + UserId: user1.Id, + CreateAt: startTime + 10, + Message: "zz" + model.NewId() + "b", } - post3 = store.Must(ss.Post().Save(post3)).(*model.Post) + post2 = store.Must(ss.Post().Save(post2)).(*model.Post) - // fetch the message exports for all three posts that user1 sent + // fetch the message exports for both posts that user1 sent messageExportMap := map[string]model.MessageExport{} if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil { t.Fatal(r1.Err) } else { messages := r1.Data.([]*model.MessageExport) - assert.Equal(t, numMessageExports+3, len(messages)) + assert.Equal(t, numMessageExports+2, len(messages)) for _, v := range messages { messageExportMap[*v.PostId] = *v @@ -415,6 +509,7 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) { assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage) assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId) assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName) + assert.Equal(t, channel.Type, *messageExportMap[post1.Id].ChannelType) assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId) assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail) assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username) @@ -425,16 +520,179 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) { assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage) assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId) assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName) + assert.Equal(t, channel.Type, *messageExportMap[post2.Id].ChannelType) assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId) assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail) assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username) +} + +func testMessageExportDirectMessageChannel(t *testing.T, ss store.Store) { + // get the starting number of message export entries + startTime := model.GetMillis() + var numMessageExports = 0 + if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil { + t.Fatal(r1.Err) + } else { + messages := r1.Data.([]*model.MessageExport) + numMessageExports = len(messages) + } + + // need a team + team := &model.Team{ + DisplayName: "DisplayName", + Name: "zz" + model.NewId() + "b", + Email: model.NewId() + "@nowhere.com", + Type: model.TEAM_OPEN, + } + team = store.Must(ss.Team().Save(team)).(*model.Team) + + // and two users that are a part of that team + user1 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + } + user1 = store.Must(ss.User().Save(user1)).(*model.User) + store.Must(ss.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: user1.Id, + }, -1)) + + user2 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + } + user2 = store.Must(ss.User().Save(user2)).(*model.User) + store.Must(ss.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: user2.Id, + }, -1)) + + // as well as a DM channel between those users + directMessageChannel := store.Must(ss.Channel().CreateDirectChannel(user1.Id, user2.Id)).(*model.Channel) + + // user1 also sends a DM to user2 + post := &model.Post{ + ChannelId: directMessageChannel.Id, + UserId: user1.Id, + CreateAt: startTime + 20, + Message: "zz" + model.NewId() + "c", + } + post = store.Must(ss.Post().Save(post)).(*model.Post) + + // fetch the message export for the post that user1 sent + messageExportMap := map[string]model.MessageExport{} + if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil { + t.Fatal(r1.Err) + } else { + messages := r1.Data.([]*model.MessageExport) + assert.Equal(t, numMessageExports+1, len(messages)) + + for _, v := range messages { + messageExportMap[*v.PostId] = *v + } + } + + // post is a DM between user1 and user2 + // there is no channel display name for direct messages, so we sub in the string "Direct Message" instead + assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId) + assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt) + assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage) + assert.Equal(t, directMessageChannel.Id, *messageExportMap[post.Id].ChannelId) + assert.Equal(t, "Direct Message", *messageExportMap[post.Id].ChannelDisplayName) + assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId) + assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail) + assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username) +} + +func testMessageExportGroupMessageChannel(t *testing.T, ss store.Store) { + // get the starting number of message export entries + startTime := model.GetMillis() + var numMessageExports = 0 + if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil { + t.Fatal(r1.Err) + } else { + messages := r1.Data.([]*model.MessageExport) + numMessageExports = len(messages) + } + + // need a team + team := &model.Team{ + DisplayName: "DisplayName", + Name: "zz" + model.NewId() + "b", + Email: model.NewId() + "@nowhere.com", + Type: model.TEAM_OPEN, + } + team = store.Must(ss.Team().Save(team)).(*model.Team) + + // and three users that are a part of that team + user1 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + } + user1 = store.Must(ss.User().Save(user1)).(*model.User) + store.Must(ss.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: user1.Id, + }, -1)) + + user2 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + } + user2 = store.Must(ss.User().Save(user2)).(*model.User) + store.Must(ss.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: user2.Id, + }, -1)) + + user3 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + } + user3 = store.Must(ss.User().Save(user3)).(*model.User) + store.Must(ss.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: user3.Id, + }, -1)) + + // can't create a group channel directly, because importing app creates an import cycle, so we have to fake it + groupMessageChannel := &model.Channel{ + TeamId: team.Id, + Name: model.NewId(), + Type: model.CHANNEL_GROUP, + } + groupMessageChannel = store.Must(ss.Channel().Save(groupMessageChannel, -1)).(*model.Channel) + + // user1 posts in the GM + post := &model.Post{ + ChannelId: groupMessageChannel.Id, + UserId: user1.Id, + CreateAt: startTime + 20, + Message: "zz" + model.NewId() + "c", + } + post = store.Must(ss.Post().Save(post)).(*model.Post) + + // fetch the message export for the post that user1 sent + messageExportMap := map[string]model.MessageExport{} + if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil { + t.Fatal(r1.Err) + } else { + messages := r1.Data.([]*model.MessageExport) + assert.Equal(t, numMessageExports+1, len(messages)) + + for _, v := range messages { + messageExportMap[*v.PostId] = *v + } + } - // post3 is a DM between user1 and user2 - assert.Equal(t, post3.Id, *messageExportMap[post3.Id].PostId) - assert.Equal(t, post3.CreateAt, *messageExportMap[post3.Id].PostCreateAt) - assert.Equal(t, post3.Message, *messageExportMap[post3.Id].PostMessage) - assert.Equal(t, directMessageChannel.Id, *messageExportMap[post3.Id].ChannelId) - assert.Equal(t, user1.Id, *messageExportMap[post3.Id].UserId) - assert.Equal(t, user1.Email, *messageExportMap[post3.Id].UserEmail) - assert.Equal(t, user1.Username, *messageExportMap[post3.Id].Username) + // post is a DM between user1 and user2 + // there is no channel display name for direct messages, so we sub in the string "Direct Message" instead + assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId) + assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt) + assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage) + assert.Equal(t, groupMessageChannel.Id, *messageExportMap[post.Id].ChannelId) + assert.Equal(t, "Group Message", *messageExportMap[post.Id].ChannelDisplayName) + assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId) + assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail) + assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username) } diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 5379c2fb4..6eab47073 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -61,6 +61,11 @@ func (_m *ChannelStore) AutocompleteInTeam(teamId string, term string) store.Sto return r0 } +// ClearCaches provides a mock function with given fields: +func (_m *ChannelStore) ClearCaches() { + _m.Called() +} + // CreateDirectChannel provides a mock function with given fields: userId, otherUserId func (_m *ChannelStore) CreateDirectChannel(userId string, otherUserId string) store.StoreChannel { ret := _m.Called(userId, otherUserId) @@ -93,22 +98,6 @@ func (_m *ChannelStore) Delete(channelId string, time int64) store.StoreChannel return r0 } -// ExtraUpdateByUser provides a mock function with given fields: userId, time -func (_m *ChannelStore) ExtraUpdateByUser(userId string, time int64) store.StoreChannel { - ret := _m.Called(userId, time) - - var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, int64) store.StoreChannel); ok { - r0 = rf(userId, time) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(store.StoreChannel) - } - } - - return r0 -} - // Get provides a mock function with given fields: id, allowFromCache func (_m *ChannelStore) Get(id string, allowFromCache bool) store.StoreChannel { ret := _m.Called(id, allowFromCache) diff --git a/store/storetest/mocks/FileInfoStore.go b/store/storetest/mocks/FileInfoStore.go index 9b479ff3a..4dddf0bd7 100644 --- a/store/storetest/mocks/FileInfoStore.go +++ b/store/storetest/mocks/FileInfoStore.go @@ -29,6 +29,11 @@ func (_m *FileInfoStore) AttachToPost(fileId string, postId string) store.StoreC return r0 } +// ClearCaches provides a mock function with given fields: +func (_m *FileInfoStore) ClearCaches() { + _m.Called() +} + // DeleteForPost provides a mock function with given fields: postId func (_m *FileInfoStore) DeleteForPost(postId string) store.StoreChannel { ret := _m.Called(postId) diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index 05e3bde34..c405d5030 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -61,6 +61,11 @@ func (_m *PostStore) AnalyticsUserCountsWithPostsByDay(teamId string) store.Stor return r0 } +// ClearCaches provides a mock function with given fields: +func (_m *PostStore) ClearCaches() { + _m.Called() +} + // Delete provides a mock function with given fields: postId, time func (_m *PostStore) Delete(postId string, time int64) store.StoreChannel { ret := _m.Called(postId, time) diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go index bdad7f81b..d38fb5f27 100644 --- a/store/storetest/mocks/TeamStore.go +++ b/store/storetest/mocks/TeamStore.go @@ -461,6 +461,22 @@ func (_m *TeamStore) UpdateDisplayName(name string, teamId string) store.StoreCh return r0 } +// UpdateLastTeamIconUpdate provides a mock function with given fields: teamId, curTime +func (_m *TeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) store.StoreChannel { + ret := _m.Called(teamId, curTime) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, int64) store.StoreChannel); ok { + r0 = rf(teamId, curTime) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // UpdateMember provides a mock function with given fields: member func (_m *TeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel { ret := _m.Called(member) diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 7d1fd8c38..369a29e7a 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -77,6 +77,11 @@ func (_m *UserStore) AnalyticsUniqueUserCount(teamId string) store.StoreChannel return r0 } +// ClearCaches provides a mock function with given fields: +func (_m *UserStore) ClearCaches() { + _m.Called() +} + // Get provides a mock function with given fields: id func (_m *UserStore) Get(id string) store.StoreChannel { ret := _m.Called(id) @@ -349,6 +354,22 @@ func (_m *UserStore) GetProfilesInChannel(channelId string, offset int, limit in return r0 } +// GetProfilesInChannelByStatus provides a mock function with given fields: channelId, offset, limit +func (_m *UserStore) GetProfilesInChannelByStatus(channelId string, offset int, limit int) store.StoreChannel { + ret := _m.Called(channelId, offset, limit) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok { + r0 = rf(channelId, offset, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // GetProfilesNotInChannel provides a mock function with given fields: teamId, channelId, offset, limit func (_m *UserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) store.StoreChannel { ret := _m.Called(teamId, channelId, offset, limit) diff --git a/store/storetest/mocks/WebhookStore.go b/store/storetest/mocks/WebhookStore.go index aa66e0600..bf5b636eb 100644 --- a/store/storetest/mocks/WebhookStore.go +++ b/store/storetest/mocks/WebhookStore.go @@ -45,6 +45,11 @@ func (_m *WebhookStore) AnalyticsOutgoingCount(teamId string) store.StoreChannel return r0 } +// ClearCaches provides a mock function with given fields: +func (_m *WebhookStore) ClearCaches() { + _m.Called() +} + // DeleteIncoming provides a mock function with given fields: webhookId, time func (_m *WebhookStore) DeleteIncoming(webhookId string, time int64) store.StoreChannel { ret := _m.Called(webhookId, time) diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index a32de9dba..cab06f87f 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -33,6 +33,7 @@ func TestTeamStore(t *testing.T, ss store.Store) { t.Run("MemberCount", func(t *testing.T) { testTeamStoreMemberCount(t, ss) }) t.Run("GetChannelUnreadsForAllTeams", func(t *testing.T) { testGetChannelUnreadsForAllTeams(t, ss) }) t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) }) + t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) }) } func testTeamStoreSave(t *testing.T, ss store.Store) { @@ -1003,3 +1004,28 @@ func testGetChannelUnreadsForTeam(t *testing.T, ss store.Store) { } } } + +func testUpdateLastTeamIconUpdate(t *testing.T, ss store.Store) { + + // team icon initially updated a second ago + lastTeamIconUpdateInitial := model.GetMillis() - 1000 + + o1 := &model.Team{} + o1.DisplayName = "Display Name" + o1.Name = "z-z-z" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + o1.LastTeamIconUpdate = lastTeamIconUpdateInitial + o1 = (<-ss.Team().Save(o1)).Data.(*model.Team) + + curTime := model.GetMillis() + + if err := (<-ss.Team().UpdateLastTeamIconUpdate(o1.Id, curTime)).Err; err != nil { + t.Fatal(err) + } + + ro1 := (<-ss.Team().Get(o1.Id)).Data.(*model.Team) + if ro1.LastTeamIconUpdate <= lastTeamIconUpdateInitial { + t.Fatal("LastTeamIconUpdate not updated") + } +} diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 47f04d1bb..2fd7d4190 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -25,6 +25,7 @@ func TestUserStore(t *testing.T, ss store.Store) { t.Run("GetAllProfiles", func(t *testing.T) { testUserStoreGetAllProfiles(t, ss) }) t.Run("GetProfiles", func(t *testing.T) { testUserStoreGetProfiles(t, ss) }) t.Run("GetProfilesInChannel", func(t *testing.T) { testUserStoreGetProfilesInChannel(t, ss) }) + t.Run("GetProfilesInChannelByStatus", func(t *testing.T) { testUserStoreGetProfilesInChannelByStatus(t, ss) }) t.Run("GetProfilesWithoutTeam", func(t *testing.T) { testUserStoreGetProfilesWithoutTeam(t, ss) }) t.Run("GetAllProfilesInChannel", func(t *testing.T) { testUserStoreGetAllProfilesInChannel(t, ss) }) t.Run("GetProfilesNotInChannel", func(t *testing.T) { testUserStoreGetProfilesNotInChannel(t, ss) }) @@ -464,6 +465,82 @@ func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) { } } +func testUserStoreGetProfilesInChannelByStatus(t *testing.T, ss store.Store) { + teamId := model.NewId() + + u1 := &model.User{} + u1.Email = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) + + u2 := &model.User{} + u2.Email = model.NewId() + store.Must(ss.User().Save(u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) + + c1 := model.Channel{} + c1.TeamId = teamId + c1.DisplayName = "Profiles in channel" + c1.Name = "profiles-" + model.NewId() + c1.Type = model.CHANNEL_OPEN + + c2 := model.Channel{} + c2.TeamId = teamId + c2.DisplayName = "Profiles in private" + c2.Name = "profiles-" + model.NewId() + c2.Type = model.CHANNEL_PRIVATE + + store.Must(ss.Channel().Save(&c1, -1)) + store.Must(ss.Channel().Save(&c2, -1)) + + m1 := model.ChannelMember{} + m1.ChannelId = c1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = c1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + m3 := model.ChannelMember{} + m3.ChannelId = c2.Id + m3.UserId = u1.Id + m3.NotifyProps = model.GetDefaultChannelNotifyProps() + + store.Must(ss.Channel().SaveMember(&m1)) + store.Must(ss.Channel().SaveMember(&m2)) + store.Must(ss.Channel().SaveMember(&m3)) + + if r1 := <-ss.User().GetProfilesInChannelByStatus(c1.Id, 0, 100); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.([]*model.User) + if len(users) != 2 { + t.Fatal("invalid returned users") + } + + found := false + for _, u := range users { + if u.Id == u1.Id { + found = true + } + } + + if !found { + t.Fatal("missing user") + } + } + + if r2 := <-ss.User().GetProfilesInChannelByStatus(c2.Id, 0, 1); r2.Err != nil { + t.Fatal(r2.Err) + } else { + if len(r2.Data.([]*model.User)) != 1 { + t.Fatal("should have returned only 1 user") + } + } +} + func testUserStoreGetProfilesWithoutTeam(t *testing.T, ss store.Store) { teamId := model.NewId() diff --git a/templates/unsupported_browser.html b/templates/unsupported_browser.html index 79656e10c..f5c515230 100644 --- a/templates/unsupported_browser.html +++ b/templates/unsupported_browser.html @@ -151,7 +151,7 @@ </li> <li> <a href="https://www.mozilla.org/en-US/firefox/new/" target="_blank" rel="noopener noreferrer"> - <span>Mozzilla Firefox 52+</span> + <span>Mozilla Firefox 52+</span> </a> </li> <li> diff --git a/utils/config.go b/utils/config.go index 679e5f62c..1b6d7714e 100644 --- a/utils/config.go +++ b/utils/config.go @@ -26,9 +26,6 @@ import ( ) const ( - MODE_DEV = "dev" - MODE_BETA = "beta" - MODE_PROD = "prod" LOG_ROTATE_SIZE = 10000 LOG_FILENAME = "mattermost.log" ) diff --git a/utils/file_backend_s3.go b/utils/file_backend_s3.go index 8e72272a1..b0601bc8a 100644 --- a/utils/file_backend_s3.go +++ b/utils/file_backend_s3.go @@ -37,7 +37,10 @@ type S3FileBackend struct { // disables automatic region lookup. func (b *S3FileBackend) s3New() (*s3.Client, error) { var creds *credentials.Credentials - if b.signV2 { + + if b.accessKey == "" && b.secretKey == "" { + creds = credentials.NewIAM("") + } else if b.signV2 { creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV2) } else { creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV4) @@ -244,3 +247,19 @@ func s3CopyMetadata(encrypt bool) map[string]string { metaData["x-amz-server-side-encryption"] = "AES256" return metaData } + +func CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError { + if len(settings.AmazonS3Bucket) == 0 { + return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest) + } + + if len(settings.AmazonS3Endpoint) == 0 { + return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_endpoint", nil, "", http.StatusBadRequest) + } + + if len(settings.AmazonS3Region) == 0 { + return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_region", nil, "", http.StatusBadRequest) + } + + return nil +} diff --git a/utils/file_backend_s3_test.go b/utils/file_backend_s3_test.go new file mode 100644 index 000000000..ff42a4d19 --- /dev/null +++ b/utils/file_backend_s3_test.go @@ -0,0 +1,32 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" +) + +func TestCheckMandatoryS3Fields(t *testing.T) { + cfg := model.FileSettings{} + + err := CheckMandatoryS3Fields(&cfg) + if err == nil || err.Message != "api.admin.test_s3.missing_s3_bucket" { + t.Fatal("should've failed with missing s3 bucket") + } + + cfg.AmazonS3Bucket = "test-mm" + err = CheckMandatoryS3Fields(&cfg) + if err == nil || err.Message != "api.admin.test_s3.missing_s3_endpoint" { + t.Fatal("should've failed with missing s3 endpoint") + } + + cfg.AmazonS3Endpoint = "s3.newendpoint.com" + err = CheckMandatoryS3Fields(&cfg) + if err == nil || err.Message != "api.admin.test_s3.missing_s3_region" { + t.Fatal("should've failed with missing s3 region") + } + +} diff --git a/utils/i18n.go b/utils/i18n.go index 71e1aaee1..8ed82d19f 100644 --- a/utils/i18n.go +++ b/utils/i18n.go @@ -91,11 +91,6 @@ func GetUserTranslations(locale string) i18n.TranslateFunc { return translations } -func SetTranslations(locale string) i18n.TranslateFunc { - translations := TfuncWithFallback(locale) - return translations -} - func GetTranslationsAndLocale(w http.ResponseWriter, r *http.Request) (i18n.TranslateFunc, string) { // This is for checking against locales like pt_BR or zn_CN headerLocaleFull := strings.Split(r.Header.Get("Accept-Language"), ",")[0] diff --git a/utils/log.go b/utils/log.go deleted file mode 100644 index c1f579e9d..000000000 --- a/utils/log.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package utils - -import ( - "bytes" - "io" - "io/ioutil" - - l4g "github.com/alecthomas/log4go" -) - -// InfoReader logs the content of the io.Reader and returns a new io.Reader -// with the same content as the received io.Reader. -// If you pass reader by reference, it won't be re-created unless the loglevel -// includes Debug. -// If an error is returned, the reader is consumed an cannot be read again. -func InfoReader(reader io.Reader, message string) (io.Reader, error) { - var err error - l4g.Info(func() string { - content, err := ioutil.ReadAll(reader) - if err != nil { - return "" - } - - reader = bytes.NewReader(content) - - return message + string(content) - }) - - return reader, err -} diff --git a/utils/logger/log4go_json_writer.go b/utils/logger/log4go_json_writer.go deleted file mode 100644 index ede541b2b..000000000 --- a/utils/logger/log4go_json_writer.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -// glue functions that allow logger.go to leverage log4Go to write JSON-formatted log records to a file - -package logger - -import ( - l4g "github.com/alecthomas/log4go" - "github.com/mattermost/mattermost-server/utils" -) - -// newJSONLogWriter is a utility method for creating a FileLogWriter set up to -// output JSON record log messages instead of line-based ones. -func newJSONLogWriter(fname string, rotate bool) *l4g.FileLogWriter { - return l4g.NewFileLogWriter(fname, rotate).SetFormat( - `{"level": "%L", - "timestamp": "%D %T", - "source": "%S", - "message": %M - }`).SetRotateLines(utils.LOG_ROTATE_SIZE) -} - -// NewJSONFileLogger - Create a new logger with a "file" filter configured to send JSON-formatted log messages at -// or above lvl to a file with the specified filename. -func NewJSONFileLogger(lvl l4g.Level, filename string) l4g.Logger { - return l4g.Logger{ - "file": &l4g.Filter{Level: lvl, LogWriter: newJSONLogWriter(filename, false)}, - } -} diff --git a/utils/logger/logger.go b/utils/logger/logger.go deleted file mode 100644 index 558f3fe47..000000000 --- a/utils/logger/logger.go +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -// this is a new logger interface for mattermost - -package logger - -import ( - "context" - "encoding/json" - "fmt" - "path/filepath" - "runtime" - - l4g "github.com/alecthomas/log4go" - - "strings" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/pkg/errors" -) - -// this pattern allows us to "mock" the underlying l4g code when unit testing -var logger l4g.Logger -var debugLog = l4g.Debug -var infoLog = l4g.Info -var errorLog = l4g.Error - -// assumes that ../config.go::configureLog has already been called, and has in turn called l4g.close() to clean up -// any old filters that we might have previously created -func initL4g(logSettings model.LogSettings) { - // TODO: add support for newConfig.LogSettings.EnableConsole. Right now, ../config.go sets it up in its configureLog - // method. If we also set it up here, messages will be written to the console twice. Eventually, when all instances - // of l4g have been replaced by this logger, we can move that code to here - if logSettings.EnableFile { - level := l4g.DEBUG - if logSettings.FileLevel == "INFO" { - level = l4g.INFO - } else if logSettings.FileLevel == "WARN" { - level = l4g.WARNING - } else if logSettings.FileLevel == "ERROR" { - level = l4g.ERROR - } - - // create a logger that writes JSON objects to a file, and override our log methods to use it - if logger != nil { - logger.Close() - } - logger = NewJSONFileLogger(level, utils.GetLogFileLocation(logSettings.FileLocation)+".jsonl") - debugLog = logger.Debug - infoLog = logger.Info - errorLog = logger.Error - } -} - -// contextKey lets us add contextual information to log messages -type contextKey string - -func (c contextKey) String() string { - return string(c) -} - -const contextKeyUserID contextKey = contextKey("user_id") -const contextKeyRequestID contextKey = contextKey("request_id") - -// any contextKeys added to this array will be serialized in every log message -var contextKeys = [2]contextKey{contextKeyUserID, contextKeyRequestID} - -// WithUserId adds a user id to the specified context. If the returned Context is subsequently passed to a logging -// method, the user id will automatically be included in the logged message -func WithUserId(ctx context.Context, userID string) context.Context { - return context.WithValue(ctx, contextKeyUserID, userID) -} - -// WithRequestId adds a request id to the specified context. If the returned Context is subsequently passed to a logging -// method, the request id will automatically be included in the logged message -func WithRequestId(ctx context.Context, requestID string) context.Context { - return context.WithValue(ctx, contextKeyRequestID, requestID) -} - -// extracts known contextKey values from the specified Context and assembles them into the returned map -func serializeContext(ctx context.Context) map[string]string { - serialized := make(map[string]string) - for _, key := range contextKeys { - value, ok := ctx.Value(key).(string) - if ok { - serialized[string(key)] = value - } - } - return serialized -} - -// Returns the path to the next file up the callstack that has a different name than this file -// in other words, finds the path to the file that is doing the logging. -// Removes machine-specific prefix, so returned path starts with /mattermost-server. -// Looks a maximum of 10 frames up the call stack to find a file that has a different name than this one. -func getCallerFilename() (string, error) { - _, currentFilename, _, ok := runtime.Caller(0) - if !ok { - return "", errors.New("Failed to traverse stack frame") - } - - platformDirectory := currentFilename - for filepath.Base(platformDirectory) != "platform" { - platformDirectory = filepath.Dir(platformDirectory) - if platformDirectory == "." || platformDirectory == string(filepath.Separator) { - break - } - } - - for i := 1; i < 10; i++ { - _, parentFilename, _, ok := runtime.Caller(i) - if !ok { - return "", errors.New("Failed to traverse stack frame") - } else if parentFilename != currentFilename && strings.Contains(parentFilename, platformDirectory) { - // trim parentFilename such that we return the path to parentFilename, relative to platformDirectory - return parentFilename[strings.LastIndex(parentFilename, platformDirectory)+len(platformDirectory)+1:], nil - } - } - return "", errors.New("Failed to traverse stack frame") -} - -// creates a JSON representation of a log message -func serializeLogMessage(ctx context.Context, message string) string { - callerFilename, err := getCallerFilename() - if err != nil { - callerFilename = "Unknown" - } - - bytes, err := json.Marshal(&struct { - Context map[string]string `json:"context"` - File string `json:"file"` - Message string `json:"message"` - }{ - serializeContext(ctx), - callerFilename, - message, - }) - if err != nil { - errorLog("Failed to serialize log message %v", message) - } - return string(bytes) -} - -func formatMessage(args ...interface{}) string { - msg, ok := args[0].(string) - if !ok { - panic("Second argument is not of type string") - } - if len(args) > 1 { - variables := args[1:] - msg = fmt.Sprintf(msg, variables...) - } - return msg -} - -// Debugc logs a debugLog level message, including context information that is stored in the first parameter. -// If two parameters are supplied, the second must be a message string, and will be logged directly. -// If more than two parameters are supplied, the second parameter must be a format string, and the remaining parameters -// must be the variables to substitute into the format string, following the convention of the fmt.Sprintf(...) function. -func Debugc(ctx context.Context, args ...interface{}) { - debugLog(func() string { - msg := formatMessage(args...) - return serializeLogMessage(ctx, msg) - }) -} - -// Debugf logs a debugLog level message. -// If one parameter is supplied, it must be a message string, and will be logged directly. -// If two or more parameters are specified, the first parameter must be a format string, and the remaining parameters -// must be the variables to substitute into the format string, following the convention of the fmt.Sprintf(...) function. -func Debugf(args ...interface{}) { - debugLog(func() string { - msg := formatMessage(args...) - return serializeLogMessage(context.Background(), msg) - }) -} - -// Infoc logs an infoLog level message, including context information that is stored in the first parameter. -// If two parameters are supplied, the second must be a message string, and will be logged directly. -// If more than two parameters are supplied, the second parameter must be a format string, and the remaining parameters -// must be the variables to substitute into the format string, following the convention of the fmt.Sprintf(...) function. -func Infoc(ctx context.Context, args ...interface{}) { - infoLog(func() string { - msg := formatMessage(args...) - return serializeLogMessage(ctx, msg) - }) -} - -// Infof logs an infoLog level message. -// If one parameter is supplied, it must be a message string, and will be logged directly. -// If two or more parameters are specified, the first parameter must be a format string, and the remaining parameters -// must be the variables to substitute into the format string, following the convention of the fmt.Sprintf(...) function. -func Infof(args ...interface{}) { - infoLog(func() string { - msg := formatMessage(args...) - return serializeLogMessage(context.Background(), msg) - }) -} - -// Errorc logs an error level message, including context information that is stored in the first parameter. -// If two parameters are supplied, the second must be a message string, and will be logged directly. -// If more than two parameters are supplied, the second parameter must be a format string, and the remaining parameters -// must be the variables to substitute into the format string, following the convention of the fmt.Sprintf(...) function. -func Errorc(ctx context.Context, args ...interface{}) { - errorLog(func() string { - msg := formatMessage(args...) - return serializeLogMessage(ctx, msg) - }) -} - -// Errorf logs an error level message. -// If one parameter is supplied, it must be a message string, and will be logged directly. -// If two or more parameters are specified, the first parameter must be a format string, and the remaining parameters -// must be the variables to substitute into the format string, following the convention of the fmt.Sprintf(...) function. -func Errorf(args ...interface{}) { - errorLog(func() string { - msg := formatMessage(args...) - return serializeLogMessage(context.Background(), msg) - }) -} diff --git a/utils/lru.go b/utils/lru.go index 576331563..8e896a6dc 100644 --- a/utils/lru.go +++ b/utils/lru.go @@ -9,15 +9,14 @@ package utils import ( "container/list" - "errors" "sync" "time" ) // Caching Interface type ObjectCache interface { - AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) bool - AddWithDefaultExpires(key, value interface{}) bool + AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) + AddWithDefaultExpires(key, value interface{}) Purge() Get(key interface{}) (value interface{}, ok bool) Remove(key interface{}) @@ -32,10 +31,11 @@ type Cache struct { evictList *list.List items map[interface{}]*list.Element lock sync.RWMutex - onEvicted func(key interface{}, value interface{}) name string defaultExpiry int64 invalidateClusterEvent string + currentGeneration int64 + len int } // entry is used to hold a value in the evictList @@ -43,25 +43,16 @@ type entry struct { key interface{} value interface{} expireAtSecs int64 + generation int64 } // New creates an LRU of the given size func NewLru(size int) *Cache { - cache, _ := NewLruWithEvict(size, nil) - return cache -} - -func NewLruWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { - if size <= 0 { - return nil, errors.New(T("utils.iru.with_evict")) - } - c := &Cache{ + return &Cache{ size: size, evictList: list.New(), items: make(map[interface{}]*list.Element, size), - onEvicted: onEvicted, } - return c, nil } func NewLruWithParams(size int, name string, defaultExpiry int64, invalidateClusterEvent string) *Cache { @@ -77,26 +68,19 @@ func (c *Cache) Purge() { c.lock.Lock() defer c.lock.Unlock() - if c.onEvicted != nil { - for k, v := range c.items { - c.onEvicted(k, v.Value) - } - } - - c.evictList = list.New() - c.items = make(map[interface{}]*list.Element, c.size) + c.len = 0 + c.currentGeneration++ } -func (c *Cache) Add(key, value interface{}) bool { - return c.AddWithExpiresInSecs(key, value, 0) +func (c *Cache) Add(key, value interface{}) { + c.AddWithExpiresInSecs(key, value, 0) } -func (c *Cache) AddWithDefaultExpires(key, value interface{}) bool { - return c.AddWithExpiresInSecs(key, value, c.defaultExpiry) +func (c *Cache) AddWithDefaultExpires(key, value interface{}) { + c.AddWithExpiresInSecs(key, value, c.defaultExpiry) } -// Add adds a value to the cache. Returns true if an eviction occurred. -func (c *Cache) AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) bool { +func (c *Cache) AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) { c.lock.Lock() defer c.lock.Unlock() @@ -107,45 +91,46 @@ func (c *Cache) AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) // Check for existing item if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) - ent.Value.(*entry).value = value - ent.Value.(*entry).expireAtSecs = expireAtSecs - return false + e := ent.Value.(*entry) + e.value = value + e.expireAtSecs = expireAtSecs + if e.generation != c.currentGeneration { + e.generation = c.currentGeneration + c.len++ + } + return } // Add new item - ent := &entry{key, value, expireAtSecs} + ent := &entry{key, value, expireAtSecs, c.currentGeneration} entry := c.evictList.PushFront(ent) c.items[key] = entry + c.len++ - evict := c.evictList.Len() > c.size - // Verify size not exceeded - if evict { - c.removeOldest() + if c.evictList.Len() > c.size { + c.removeElement(c.evictList.Back()) } - return evict } -// Get looks up a key's value from the cache. func (c *Cache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() defer c.lock.Unlock() if ent, ok := c.items[key]; ok { + e := ent.Value.(*entry) - if ent.Value.(*entry).expireAtSecs > 0 { - if (time.Now().UnixNano() / int64(time.Second)) > ent.Value.(*entry).expireAtSecs { - c.removeElement(ent) - return nil, false - } + if e.generation != c.currentGeneration || (e.expireAtSecs > 0 && (time.Now().UnixNano()/int64(time.Second)) > e.expireAtSecs) { + c.removeElement(ent) + return nil, false } c.evictList.MoveToFront(ent) return ent.Value.(*entry).value, true } - return + + return nil, false } -// Remove removes the provided key from the cache. func (c *Cache) Remove(key interface{}) { c.lock.Lock() defer c.lock.Unlock() @@ -155,25 +140,19 @@ func (c *Cache) Remove(key interface{}) { } } -// RemoveOldest removes the oldest item from the cache. -func (c *Cache) RemoveOldest() { - c.lock.Lock() - defer c.lock.Unlock() - c.removeOldest() -} - // Keys returns a slice of the keys in the cache, from oldest to newest. func (c *Cache) Keys() []interface{} { c.lock.RLock() defer c.lock.RUnlock() - keys := make([]interface{}, len(c.items)) - ent := c.evictList.Back() + keys := make([]interface{}, c.len) i := 0 - for ent != nil { - keys[i] = ent.Value.(*entry).key - ent = ent.Prev() - i++ + for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() { + e := ent.Value.(*entry) + if e.generation == c.currentGeneration { + keys[i] = e.key + i++ + } } return keys @@ -183,7 +162,7 @@ func (c *Cache) Keys() []interface{} { func (c *Cache) Len() int { c.lock.RLock() defer c.lock.RUnlock() - return c.evictList.Len() + return c.len } func (c *Cache) Name() string { @@ -194,20 +173,12 @@ func (c *Cache) GetInvalidateClusterEvent() string { return c.invalidateClusterEvent } -// removeOldest removes the oldest item from the cache. -func (c *Cache) removeOldest() { - ent := c.evictList.Back() - if ent != nil { - c.removeElement(ent) - } -} - // removeElement is used to remove a given list element from the cache func (c *Cache) removeElement(e *list.Element) { c.evictList.Remove(e) kv := e.Value.(*entry) - delete(c.items, kv.key) - if c.onEvicted != nil { - c.onEvicted(kv.key, kv.value) + if kv.generation == c.currentGeneration { + c.len-- } + delete(c.items, kv.key) } diff --git a/utils/lru_test.go b/utils/lru_test.go index 987163cd3..4312515b9 100644 --- a/utils/lru_test.go +++ b/utils/lru_test.go @@ -11,14 +11,7 @@ import "testing" import "time" func TestLRU(t *testing.T) { - evictCounter := 0 - onEvicted := func(k interface{}, v interface{}) { - evictCounter += 1 - } - l, err := NewLruWithEvict(128, onEvicted) - if err != nil { - t.Fatalf("err: %v", err) - } + l := NewLru(128) for i := 0; i < 256; i++ { l.Add(i, i) @@ -27,10 +20,6 @@ func TestLRU(t *testing.T) { t.Fatalf("bad len: %v", l.Len()) } - if evictCounter != 128 { - t.Fatalf("bad evict count: %v", evictCounter) - } - for i, k := range l.Keys() { if v, ok := l.Get(k); !ok || v != k || v != i+128 { t.Fatalf("bad key: %v", k) @@ -73,26 +62,6 @@ func TestLRU(t *testing.T) { } } -// test that Add return true/false if an eviction occurred -func TestLRUAdd(t *testing.T) { - evictCounter := 0 - onEvicted := func(k interface{}, v interface{}) { - evictCounter += 1 - } - - l, err := NewLruWithEvict(1, onEvicted) - if err != nil { - t.Fatalf("err: %v", err) - } - - if l.Add(1, 1) || evictCounter != 0 { - t.Errorf("should not have an eviction") - } - if !l.Add(2, 2) || evictCounter != 1 { - t.Errorf("should have an eviction") - } -} - func TestLRUExpire(t *testing.T) { l := NewLru(128) diff --git a/utils/mail.go b/utils/mail.go index 9023f7090..3b9f4bd9d 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -5,6 +5,8 @@ package utils import ( "crypto/tls" + "errors" + "io" "mime" "net" "net/mail" @@ -15,8 +17,6 @@ import ( "net/http" - "io" - l4g "github.com/alecthomas/log4go" "github.com/mattermost/html2text" "github.com/mattermost/mattermost-server/model" @@ -26,6 +26,56 @@ func encodeRFC2047Word(s string) string { return mime.BEncoding.Encode("utf-8", s) } +type authChooser struct { + smtp.Auth + Config *model.Config +} + +func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) { + a.Auth = LoginAuth(a.Config.EmailSettings.SMTPUsername, a.Config.EmailSettings.SMTPPassword, a.Config.EmailSettings.SMTPServer+":"+a.Config.EmailSettings.SMTPPort) + for _, method := range server.Auth { + if method == "PLAIN" { + a.Auth = smtp.PlainAuth("", a.Config.EmailSettings.SMTPUsername, a.Config.EmailSettings.SMTPPassword, a.Config.EmailSettings.SMTPServer+":"+a.Config.EmailSettings.SMTPPort) + break + } + } + return a.Auth.Start(server) +} + +type loginAuth struct { + username, password, host string +} + +func LoginAuth(username, password, host string) smtp.Auth { + return &loginAuth{username, password, host} +} + +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + if !server.TLS { + return "", nil, errors.New("unencrypted connection") + } + + if server.Name != a.host { + return "", nil, errors.New("wrong host name") + } + + return "LOGIN", []byte{}, nil +} + +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + return nil, errors.New("Unkown fromServer") + } + } + return nil, nil +} + func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { var conn net.Conn var err error @@ -75,9 +125,7 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap } if *config.EmailSettings.EnableSMTPAuth { - auth := smtp.PlainAuth("", config.EmailSettings.SMTPUsername, config.EmailSettings.SMTPPassword, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort) - - if err = c.Auth(auth); err != nil { + if err = c.Auth(&authChooser{Config: config}); err != nil { return nil, model.NewAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error(), http.StatusInternalServerError) } } @@ -138,10 +186,8 @@ func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string "Auto-Submitted": {"auto-generated"}, "Precedence": {"bulk"}, } - if mimeHeaders != nil { - for k, v := range mimeHeaders { - headers[k] = []string{encodeRFC2047Word(v)} - } + for k, v := range mimeHeaders { + headers[k] = []string{encodeRFC2047Word(v)} } m := gomail.NewMessage(gomail.SetCharset("UTF-8")) diff --git a/utils/mail_test.go b/utils/mail_test.go index 068c90c60..31a4f8996 100644 --- a/utils/mail_test.go +++ b/utils/mail_test.go @@ -7,6 +7,9 @@ import ( "strings" "testing" + "net/smtp" + + "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/require" ) @@ -169,3 +172,64 @@ func TestSendMailUsingConfig(t *testing.T) { } } }*/ + +func TestAuthMethods(t *testing.T) { + config := model.Config{ + EmailSettings: model.EmailSettings{ + EnableSMTPAuth: model.NewBool(false), + SMTPUsername: "test", + SMTPPassword: "fakepass", + SMTPServer: "fakeserver", + SMTPPort: "25", + }, + } + + auth := &authChooser{Config: &config} + tests := []struct { + desc string + server *smtp.ServerInfo + err string + }{ + { + desc: "auth PLAIN success", + server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"PLAIN"}, TLS: true}, + }, + { + desc: "auth PLAIN unencrypted connection fail", + server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"PLAIN"}, TLS: false}, + err: "unencrypted connection", + }, + { + desc: "auth PLAIN wrong host name", + server: &smtp.ServerInfo{Name: "wrongServer:999", Auth: []string{"PLAIN"}, TLS: true}, + err: "wrong host name", + }, + { + desc: "auth LOGIN success", + server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"LOGIN"}, TLS: true}, + }, + { + desc: "auth LOGIN unencrypted connection fail", + server: &smtp.ServerInfo{Name: "wrongServer:999", Auth: []string{"LOGIN"}, TLS: true}, + err: "wrong host name", + }, + { + desc: "auth LOGIN wrong host name", + server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"LOGIN"}, TLS: false}, + err: "unencrypted connection", + }, + } + + for i, test := range tests { + t.Run(test.desc, func(t *testing.T) { + _, _, err := auth.Start(test.server) + got := "" + if err != nil { + got = err.Error() + } + if got != test.err { + t.Errorf("%d. got error = %q; want %q", i, got, test.err) + } + }) + } +} diff --git a/vendor/github.com/avct/uasurfer/.gitignore b/vendor/github.com/avct/uasurfer/.gitignore new file mode 100644 index 000000000..35ba52a16 --- /dev/null +++ b/vendor/github.com/avct/uasurfer/.gitignore @@ -0,0 +1,56 @@ +# Compiled bin # +################### + + +# Compiled source # +################### +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Configuration Files # +####################### +*.cfg + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite +logs +coverage.html +coverage.out + +# Test Files # +####################### +*.test + +# OS generated files # +###################### +.DS_Store +.DS_Store? +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# go.rice generated files +*.rice-box.go + +# Dev Tools # +###################### +.vagrant
\ No newline at end of file diff --git a/vendor/github.com/avct/uasurfer/.travis.yml b/vendor/github.com/avct/uasurfer/.travis.yml new file mode 100644 index 000000000..77b64e6f2 --- /dev/null +++ b/vendor/github.com/avct/uasurfer/.travis.yml @@ -0,0 +1,11 @@ +sudo: false + +language: go + +go: + - 1.9.x + - 1.8.x + - 1.7.x + +script: + - go test diff --git a/vendor/github.com/avct/uasurfer/LICENSE b/vendor/github.com/avct/uasurfer/LICENSE new file mode 100644 index 000000000..a092343b2 --- /dev/null +++ b/vendor/github.com/avct/uasurfer/LICENSE @@ -0,0 +1,192 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Copyright 2015 Avocet Systems Ltd. + http://avocet.io/opensource + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + Copyright 2015 Avocet Systems Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.
\ No newline at end of file diff --git a/vendor/github.com/avct/uasurfer/README.md b/vendor/github.com/avct/uasurfer/README.md new file mode 100644 index 000000000..2a4ab608d --- /dev/null +++ b/vendor/github.com/avct/uasurfer/README.md @@ -0,0 +1,169 @@ +[![Build Status](https://travis-ci.org/avct/uasurfer.svg?branch=master)](https://travis-ci.org/avct/uasurfer) [![GoDoc](https://godoc.org/github.com/avct/uasurfer?status.svg)](https://godoc.org/github.com/avct/uasurfer) [![Go Report Card](https://goreportcard.com/badge/github.com/avct/uasurfer)](https://goreportcard.com/report/github.com/avct/uasurfer) + +# uasurfer + +![uasurfer-100px](https://cloud.githubusercontent.com/assets/597902/16172506/9debc136-357a-11e6-90fb-c7c46f50dff0.png) + +**User Agent Surfer** (uasurfer) is a lightweight Golang package that parses and abstracts [HTTP User-Agent strings](https://en.wikipedia.org/wiki/User_agent) with particular attention to device type. + +The following information is returned by uasurfer from a raw HTTP User-Agent string: + +| Name | Example | Coverage in 192,792 parses | +|----------------|---------|--------------------------------| +| Browser name | `chrome` | 99.85% | +| Browser version | `53` | 99.17% | +| Platform | `ipad` | 99.97% | +| OS name | `ios` | 99.96% | +| OS version | `10` | 98.81% | +| Device type | `tablet` | 99.98% | + +Layout engine, browser language, and other esoteric attributes are not parsed. + +Coverage is estimated from a random sample of real UA strings collected across thousands of sources in US and EU mid-2016. + +## Usage + +### Parse(ua string) Function + +The `Parse()` function accepts a user agent `string` and returns UserAgent struct with named constants and integers for versions (minor, major and patch separately), and the full UA string that was parsed (lowercase). A string can be retrieved by adding `.String()` to a variable, such as `uasurfer.BrowserName.String()`. + +``` +// Define a user agent string +myUA := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36" + +// Parse() returns all attributes, including returning the full UA string last +ua, uaString := uasurfer.Parse(myUA) +``` + +where example UserAgent is: +``` +{ + Browser { + BrowserName: BrowserChrome, + Version: { + Major: 45, + Minor: 0, + Patch: 2454, + }, + }, + OS { + Platform: PlatformMac, + Name: OSMacOSX, + Version: { + Major: 10, + Minor: 10, + Patch: 5, + }, + }, + DeviceType: DeviceComputer, +} +``` + +**Usage note:** There are some OSes that do not return a version, see docs below. Linux is typically not reported with a specific Linux distro name or version. + +#### Browser Name +* `BrowserChrome` - Google [Chrome](https://en.wikipedia.org/wiki/Google_Chrome), [Chromium](https://en.wikipedia.org/wiki/Chromium_(web_browser)) +* `BrowserSafari` - Apple [Safari](https://en.wikipedia.org/wiki/Safari_(web_browser)), Google Search ([GSA](https://itunes.apple.com/us/app/google/id284815942)) +* `BrowserIE` - Microsoft [Internet Explorer](https://en.wikipedia.org/wiki/Internet_Explorer), [Edge](https://en.wikipedia.org/wiki/Microsoft_Edge) +* `BrowserFirefox` - Mozilla [Firefox](https://en.wikipedia.org/wiki/Firefox), GNU [IceCat](https://en.wikipedia.org/wiki/GNU_IceCat), [Iceweasel](https://en.wikipedia.org/wiki/Mozilla_Corporation_software_rebranded_by_the_Debian_project#Iceweasel), [Seamonkey](https://en.wikipedia.org/wiki/SeaMonkey) +* `BrowserAndroid` - Android [WebView](https://developer.chrome.com/multidevice/webview/overview) (Android OS <4.4 only) +* `BrowserOpera` - [Opera](https://en.wikipedia.org/wiki/Opera_(web_browser)) +* `BrowserUCBrowser` - [UC Browser](https://en.wikipedia.org/wiki/UC_Browser) +* `BrowserSilk` - Amazon [Silk](https://en.wikipedia.org/wiki/Amazon_Silk) +* `BrowserSpotify` - [Spotify](https://en.wikipedia.org/wiki/Spotify#Clients) desktop client +* `BrowserBlackberry` - RIM [BlackBerry](https://en.wikipedia.org/wiki/BlackBerry) +* `BrowserUnknown` - Unknown + +#### Browser Version + +Browser version returns an `unint8` of the major version attribute of the User-Agent String. For example Chrome 45.0.23423 would return `45`. The intention is to support math operators with versions, such as "do XYZ for Chrome version >23". + +Unknown version is returned as `0`. + +#### Platform +* `PlatformWindows` - Microsoft Windows +* `PlatformMac` - Apple Macintosh +* `PlatformLinux` - Linux, including Android and other OSes +* `PlatformiPad` - Apple iPad +* `PlatformiPhone` - Apple iPhone +* `PlatformBlackberry` - RIM Blackberry +* `PlatformWindowsPhone` Microsoft Windows Phone & Mobile +* `PlatformKindle` - Amazon Kindle & Kindle Fire +* `PlatformPlaystation` - Sony Playstation, Vita, PSP +* `PlatformXbox` - Microsoft Xbox - `PlatformXbox` +* `PlatformNintendo` - Nintendo DS, Wii, etc. +* `PlatformUnknown` - Unknown + +#### OS Name +* `OSWindows` +* `OSMacOSX` - includes "macOS Sierra" +* `OSiOS` +* `OSAndroid` +* `OSChromeOS` +* `OSWebOS` +* `OSLinux` +* `OSPlaystation` +* `OSXbox` +* `OSNintendo` +* `OSUnknown` + +#### OS Version + +OS X major version is alway 10 with consecutive minor versions indicating release releases (10 - Yosemite, 11 - El Capitain, 12 Sierra, etc). Windows version is NT version. `Version{0, 0, 0}` indicated version is unknown or not evaluated. +Versions can be compared using `Less` function: `if ver1.Less(ver2) {}` + +Here are some examples across the platform, os.name, and os.version: + +* For Windows XP (Windows NT 5.1), "`PlatformWindows`" is the platform, "`OSWindows`" is the name, and `{5, 1, 0}` the version. +* For OS X 10.5.1, "`PlatformMac`" is the platform, "`OSMacOSX`" the name, and `{10, 5, 1}` the version. +* For Android 5.1, "`PlatformLinux`" is the platform, "`OSAndroid`" is the name, and `{5, 1, 0}` the version. +* For iOS 5.1, "`PlatformiPhone`" or "`PlatformiPad`" is the platform, "`OSiOS`" is the name, and `{5, 1, 0}` the version. + +###### Windows Version Guide + +* Windows 10 - `{10, 0, 0}` +* Windows 8.1 - `{6, 3, 0}` +* Windows 8 - `{6, 2, 0}` +* Windows 7 - `{6, 1, 0}` +* Windows Vista - `{6, 0, 0}` +* Windows XP - `{5, 1, 0}` or `{5, 2, 0}` +* Windows 2000 - `{5, 0, 0}` + +Windows 95, 98, and ME represent 0.01% of traffic worldwide and are not available through this package at this time. + +#### DeviceType +DeviceType is typically quite accurate, though determining between phones and tablets on Android is not always possible due to how some vendors design their UA strings. A mobile Android device without tablet indicator defaults to being classified as a phone. DeviceTV supports major brands such as Philips, Sharp, Vizio and steaming boxes such as Apple, Google, Roku, Amazon. + +* `DeviceComputer` +* `DevicePhone` +* `DeviceTablet` +* `DeviceTV` +* `DeviceConsole` +* `DeviceWearable` +* `DeviceUnknown` + +## Example Combinations of Attributes +* Surface RT -> `OSWindows8`, `DeviceTablet`, OSVersion >= `6` +* Android Tablet -> `OSAndroid`, `DeviceTablet` +* Microsoft Edge -> `BrowserIE`, BrowserVersion >= `12.0.0` + +## To do + +* Remove compiled regexp in favor of string.Contains wherever possible (lowers mem/alloc) +* Better version support on Firefox derivatives (e.g. SeaMonkey) +* Potential additional browser support: + * "NetFront" (1% share in India) + * "QQ Browser" (6.5% share in China) + * "Sogou Explorer" (5% share in China) + * "Maxthon" (1.5% share in China) + * "Nokia" +* Potential additional OS support: + * "Nokia" (5% share in India) + * "Series 40" (5.5% share in India) + * Windows 2003 Server +* iOS safari browser identification based on iOS version +* Add android version to browser identification +* old Macs + * "opera/9.64 (macintosh; ppc mac os x; u; en) presto/2.1.1" +* old Windows + * "mozilla/5.0 (windows nt 4.0; wow64) applewebkit/537.36 (khtml, like gecko) chrome/37.0.2049.0 safari/537.36" diff --git a/vendor/github.com/avct/uasurfer/browser.go b/vendor/github.com/avct/uasurfer/browser.go new file mode 100644 index 000000000..e156818ab --- /dev/null +++ b/vendor/github.com/avct/uasurfer/browser.go @@ -0,0 +1,192 @@ +package uasurfer + +import ( + "strings" +) + +// Browser struct contains the lowercase name of the browser, along +// with its browser version number. Browser are grouped together without +// consideration for device. For example, Chrome (Chrome/43.0) and Chrome for iOS +// (CriOS/43.0) would both return as "chrome" (name) and 43.0 (version). Similarly +// Internet Explorer 11 and Edge 12 would return as "ie" and "11" or "12", respectively. +// type Browser struct { +// Name BrowserName +// Version struct { +// Major int +// Minor int +// Patch int +// } +// } + +// Retrieve browser name from UA strings +func (u *UserAgent) evalBrowserName(ua string) bool { + // Blackberry goes first because it reads as MSIE & Safari + if strings.Contains(ua, "blackberry") || strings.Contains(ua, "playbook") || strings.Contains(ua, "bb10") || strings.Contains(ua, "rim ") { + u.Browser.Name = BrowserBlackberry + return u.isBot() + } + + if strings.Contains(ua, "applewebkit") { + switch { + case strings.Contains(ua, "opr/") || strings.Contains(ua, "opios/"): + u.Browser.Name = BrowserOpera + + case strings.Contains(ua, "silk/"): + u.Browser.Name = BrowserSilk + + case strings.Contains(ua, "edge/") || strings.Contains(ua, "iemobile/") || strings.Contains(ua, "msie "): + u.Browser.Name = BrowserIE + + case strings.Contains(ua, "ucbrowser/") || strings.Contains(ua, "ucweb/"): + u.Browser.Name = BrowserUCBrowser + + // Edge, Silk and other chrome-identifying browsers must evaluate before chrome, unless we want to add more overhead + case strings.Contains(ua, "chrome/") || strings.Contains(ua, "crios/") || strings.Contains(ua, "chromium/") || strings.Contains(ua, "crmo/"): + u.Browser.Name = BrowserChrome + + case strings.Contains(ua, "android") && !strings.Contains(ua, "chrome/") && strings.Contains(ua, "version/") && !strings.Contains(ua, "like android"): + // Android WebView on Android >= 4.4 is purposefully being identified as Chrome above -- https://developer.chrome.com/multidevice/webview/overview + u.Browser.Name = BrowserAndroid + + case strings.Contains(ua, "fxios"): + u.Browser.Name = BrowserFirefox + + case strings.Contains(ua, " spotify/"): + u.Browser.Name = BrowserSpotify + + // AppleBot uses webkit signature as well + case strings.Contains(ua, "applebot"): + u.Browser.Name = BrowserAppleBot + + // presume it's safari unless an esoteric browser is being specified (webOSBrowser, SamsungBrowser, etc.) + case strings.Contains(ua, "like gecko") && strings.Contains(ua, "mozilla/") && strings.Contains(ua, "safari/") && !strings.Contains(ua, "linux") && !strings.Contains(ua, "android") && !strings.Contains(ua, "browser/") && !strings.Contains(ua, "os/"): + u.Browser.Name = BrowserSafari + + // if we got this far and the device is iPhone or iPad, assume safari. Some agents don't actually contain the word "safari" + case strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad"): + u.Browser.Name = BrowserSafari + + // Google's search app on iPhone, leverages native Safari rather than Chrome + case strings.Contains(ua, " gsa/"): + u.Browser.Name = BrowserSafari + + default: + goto notwebkit + + } + return u.isBot() + } + +notwebkit: + switch { + case strings.Contains(ua, "msie") || strings.Contains(ua, "trident"): + u.Browser.Name = BrowserIE + + case strings.Contains(ua, "gecko") && (strings.Contains(ua, "firefox") || strings.Contains(ua, "iceweasel") || strings.Contains(ua, "seamonkey") || strings.Contains(ua, "icecat")): + u.Browser.Name = BrowserFirefox + + case strings.Contains(ua, "presto") || strings.Contains(ua, "opera"): + u.Browser.Name = BrowserOpera + + case strings.Contains(ua, "ucbrowser"): + u.Browser.Name = BrowserUCBrowser + + case strings.Contains(ua, "applebot"): + u.Browser.Name = BrowserAppleBot + + case strings.Contains(ua, "baiduspider"): + u.Browser.Name = BrowserBaiduBot + + case strings.Contains(ua, "adidxbot") || strings.Contains(ua, "bingbot") || strings.Contains(ua, "bingpreview"): + u.Browser.Name = BrowserBingBot + + case strings.Contains(ua, "duckduckbot"): + u.Browser.Name = BrowserDuckDuckGoBot + + case strings.Contains(ua, "facebot") || strings.Contains(ua, "facebookexternalhit"): + u.Browser.Name = BrowserFacebookBot + + case strings.Contains(ua, "googlebot"): + u.Browser.Name = BrowserGoogleBot + + case strings.Contains(ua, "linkedinbot"): + u.Browser.Name = BrowserLinkedInBot + + case strings.Contains(ua, "msnbot"): + u.Browser.Name = BrowserMsnBot + + case strings.Contains(ua, "pingdom.com_bot"): + u.Browser.Name = BrowserPingdomBot + + case strings.Contains(ua, "twitterbot"): + u.Browser.Name = BrowserTwitterBot + + case strings.Contains(ua, "yandex") || strings.Contains(ua, "yadirectfetcher"): + u.Browser.Name = BrowserYandexBot + + case strings.Contains(ua, "yahoo"): + u.Browser.Name = BrowserYahooBot + + case strings.Contains(ua, "phantomjs"): + u.Browser.Name = BrowserBot + + default: + u.Browser.Name = BrowserUnknown + + } + + return u.isBot() +} + +// Retrieve browser version +// Methods used in order: +// 1st: look for generic version/# +// 2nd: look for browser-specific instructions (e.g. chrome/34) +// 3rd: infer from OS (iOS only) +func (u *UserAgent) evalBrowserVersion(ua string) { + // if there is a 'version/#' attribute with numeric version, use it -- except for Chrome since Android vendors sometimes hijack version/# + if u.Browser.Name != BrowserChrome && u.Browser.Version.findVersionNumber(ua, "version/") { + return + } + + switch u.Browser.Name { + case BrowserChrome: + // match both chrome and crios + _ = u.Browser.Version.findVersionNumber(ua, "chrome/") || u.Browser.Version.findVersionNumber(ua, "crios/") || u.Browser.Version.findVersionNumber(ua, "crmo/") + + case BrowserIE: + if u.Browser.Version.findVersionNumber(ua, "msie ") || u.Browser.Version.findVersionNumber(ua, "edge/") { + return + } + + // get MSIE version from trident version https://en.wikipedia.org/wiki/Trident_(layout_engine) + if u.Browser.Version.findVersionNumber(ua, "trident/") { + // convert trident versions 3-7 to MSIE version + if (u.Browser.Version.Major >= 3) && (u.Browser.Version.Major <= 7) { + u.Browser.Version.Major += 4 + } + } + + case BrowserFirefox: + _ = u.Browser.Version.findVersionNumber(ua, "firefox/") || u.Browser.Version.findVersionNumber(ua, "fxios/") + + case BrowserSafari: // executes typically if we're on iOS and not using a familiar browser + u.Browser.Version = u.OS.Version + // early Safari used a version number +1 to OS version + if (u.Browser.Version.Major <= 3) && (u.Browser.Version.Major >= 1) { + u.Browser.Version.Major++ + } + + case BrowserUCBrowser: + _ = u.Browser.Version.findVersionNumber(ua, "ucbrowser/") + + case BrowserOpera: + _ = u.Browser.Version.findVersionNumber(ua, "opr/") || u.Browser.Version.findVersionNumber(ua, "opios/") || u.Browser.Version.findVersionNumber(ua, "opera/") + + case BrowserSilk: + _ = u.Browser.Version.findVersionNumber(ua, "silk/") + + case BrowserSpotify: + _ = u.Browser.Version.findVersionNumber(ua, "spotify/") + } +} diff --git a/vendor/github.com/avct/uasurfer/const_string.go b/vendor/github.com/avct/uasurfer/const_string.go new file mode 100644 index 000000000..2fa21d86d --- /dev/null +++ b/vendor/github.com/avct/uasurfer/const_string.go @@ -0,0 +1,49 @@ +// Code generated by "stringer -type=DeviceType,BrowserName,OSName,Platform -output=const_string.go"; DO NOT EDIT. + +package uasurfer + +import "fmt" + +const _DeviceType_name = "DeviceUnknownDeviceComputerDeviceTabletDevicePhoneDeviceConsoleDeviceWearableDeviceTV" + +var _DeviceType_index = [...]uint8{0, 13, 27, 39, 50, 63, 77, 85} + +func (i DeviceType) String() string { + if i < 0 || i >= DeviceType(len(_DeviceType_index)-1) { + return fmt.Sprintf("DeviceType(%d)", i) + } + return _DeviceType_name[_DeviceType_index[i]:_DeviceType_index[i+1]] +} + +const _BrowserName_name = "BrowserUnknownBrowserChromeBrowserIEBrowserSafariBrowserFirefoxBrowserAndroidBrowserOperaBrowserBlackberryBrowserUCBrowserBrowserSilkBrowserNokiaBrowserNetFrontBrowserQQBrowserMaxthonBrowserSogouExplorerBrowserSpotifyBrowserBotBrowserAppleBotBrowserBaiduBotBrowserBingBotBrowserDuckDuckGoBotBrowserFacebookBotBrowserGoogleBotBrowserLinkedInBotBrowserMsnBotBrowserPingdomBotBrowserTwitterBotBrowserYandexBotBrowserYahooBot" + +var _BrowserName_index = [...]uint16{0, 14, 27, 36, 49, 63, 77, 89, 106, 122, 133, 145, 160, 169, 183, 203, 217, 227, 242, 257, 271, 291, 309, 325, 343, 356, 373, 390, 406, 421} + +func (i BrowserName) String() string { + if i < 0 || i >= BrowserName(len(_BrowserName_index)-1) { + return fmt.Sprintf("BrowserName(%d)", i) + } + return _BrowserName_name[_BrowserName_index[i]:_BrowserName_index[i+1]] +} + +const _OSName_name = "OSUnknownOSWindowsPhoneOSWindowsOSMacOSXOSiOSOSAndroidOSBlackberryOSChromeOSOSKindleOSWebOSOSLinuxOSPlaystationOSXboxOSNintendoOSBot" + +var _OSName_index = [...]uint8{0, 9, 23, 32, 40, 45, 54, 66, 76, 84, 91, 98, 111, 117, 127, 132} + +func (i OSName) String() string { + if i < 0 || i >= OSName(len(_OSName_index)-1) { + return fmt.Sprintf("OSName(%d)", i) + } + return _OSName_name[_OSName_index[i]:_OSName_index[i+1]] +} + +const _Platform_name = "PlatformUnknownPlatformWindowsPlatformMacPlatformLinuxPlatformiPadPlatformiPhonePlatformiPodPlatformBlackberryPlatformWindowsPhonePlatformPlaystationPlatformXboxPlatformNintendoPlatformBot" + +var _Platform_index = [...]uint8{0, 15, 30, 41, 54, 66, 80, 92, 110, 130, 149, 161, 177, 188} + +func (i Platform) String() string { + if i < 0 || i >= Platform(len(_Platform_index)-1) { + return fmt.Sprintf("Platform(%d)", i) + } + return _Platform_name[_Platform_index[i]:_Platform_index[i+1]] +} diff --git a/vendor/github.com/avct/uasurfer/device.go b/vendor/github.com/avct/uasurfer/device.go new file mode 100644 index 000000000..70c00b112 --- /dev/null +++ b/vendor/github.com/avct/uasurfer/device.go @@ -0,0 +1,60 @@ +package uasurfer + +import ( + "strings" +) + +func (u *UserAgent) evalDevice(ua string) { + switch { + + case u.OS.Platform == PlatformWindows || u.OS.Platform == PlatformMac || u.OS.Name == OSChromeOS: + if strings.Contains(ua, "mobile") || strings.Contains(ua, "touch") { + u.DeviceType = DeviceTablet // windows rt, linux haxor tablets + return + } + u.DeviceType = DeviceComputer + + case u.OS.Platform == PlatformiPad || u.OS.Platform == PlatformiPod || strings.Contains(ua, "tablet") || strings.Contains(ua, "kindle/") || strings.Contains(ua, "playbook"): + u.DeviceType = DeviceTablet + + case u.OS.Platform == PlatformiPhone || u.OS.Platform == PlatformBlackberry || strings.Contains(ua, "phone"): + u.DeviceType = DevicePhone + + // long list of smarttv and tv dongle identifiers + case strings.Contains(ua, "tv") || strings.Contains(ua, "crkey") || strings.Contains(ua, "googletv") || strings.Contains(ua, "aftb") || strings.Contains(ua, "adt-") || strings.Contains(ua, "roku") || strings.Contains(ua, "viera") || strings.Contains(ua, "aquos") || strings.Contains(ua, "dtv") || strings.Contains(ua, "appletv") || strings.Contains(ua, "smarttv") || strings.Contains(ua, "tuner") || strings.Contains(ua, "smart-tv") || strings.Contains(ua, "hbbtv") || strings.Contains(ua, "netcast") || strings.Contains(ua, "vizio"): + u.DeviceType = DeviceTV + + case u.OS.Name == OSAndroid: + // android phones report as "mobile", android tablets should not but often do -- http://android-developers.blogspot.com/2010/12/android-browser-user-agent-issues.html + if strings.Contains(ua, "mobile") { + u.DeviceType = DevicePhone + return + } + + if strings.Contains(ua, "tablet") || strings.Contains(ua, "nexus 7") || strings.Contains(ua, "nexus 9") || strings.Contains(ua, "nexus 10") || strings.Contains(ua, "xoom") { + u.DeviceType = DeviceTablet + return + } + + u.DeviceType = DevicePhone // default to phone + + case u.OS.Platform == PlatformPlaystation || u.OS.Platform == PlatformXbox || u.OS.Platform == PlatformNintendo: + u.DeviceType = DeviceConsole + + case strings.Contains(ua, "glass") || strings.Contains(ua, "watch") || strings.Contains(ua, "sm-v"): + u.DeviceType = DeviceWearable + + // specifically above "mobile" string check as Kindle Fire tablets report as "mobile" + case u.Browser.Name == BrowserSilk || u.OS.Name == OSKindle && !strings.Contains(ua, "sd4930ur"): + u.DeviceType = DeviceTablet + + case strings.Contains(ua, "mobile") || strings.Contains(ua, "touch") || strings.Contains(ua, " mobi") || strings.Contains(ua, "webos"): //anything "mobile"/"touch" that didn't get captured as tablet, console or wearable is presumed a phone + u.DeviceType = DevicePhone + + case u.OS.Name == OSLinux: // linux goes last since it's in so many other device types (tvs, wearables, android-based stuff) + u.DeviceType = DeviceComputer + + default: + u.DeviceType = DeviceUnknown + } +} diff --git a/vendor/github.com/avct/uasurfer/system.go b/vendor/github.com/avct/uasurfer/system.go new file mode 100644 index 000000000..e823c9cc7 --- /dev/null +++ b/vendor/github.com/avct/uasurfer/system.go @@ -0,0 +1,336 @@ +package uasurfer + +import ( + "regexp" + "strconv" + "strings" +) + +var ( + amazonFireFingerprint = regexp.MustCompile("\\s(k[a-z]{3,5}|sd\\d{4}ur)\\s") //tablet or phone +) + +func (u *UserAgent) evalOS(ua string) bool { + + s := strings.IndexRune(ua, '(') + e := strings.IndexRune(ua, ')') + if s > e { + s = 0 + e = len(ua) + } + if e == -1 { + e = len(ua) + } + + agentPlatform := ua[s+1 : e] + specsEnd := strings.Index(agentPlatform, ";") + var specs string + if specsEnd != -1 { + specs = agentPlatform[:specsEnd] + } else { + specs = agentPlatform + } + + //strict OS & version identification + switch specs { + case "android": + u.evalLinux(ua, agentPlatform) + + case "bb10", "playbook": + u.OS.Platform = PlatformBlackberry + u.OS.Name = OSBlackberry + + case "x11", "linux": + u.evalLinux(ua, agentPlatform) + + case "ipad", "iphone", "ipod touch", "ipod": + u.evaliOS(specs, agentPlatform) + + case "macintosh": + u.evalMacintosh(ua) + + default: + switch { + // Blackberry + case strings.Contains(ua, "blackberry") || strings.Contains(ua, "playbook"): + u.OS.Platform = PlatformBlackberry + u.OS.Name = OSBlackberry + + // Windows Phone + case strings.Contains(agentPlatform, "windows phone "): + u.evalWindowsPhone(agentPlatform) + + // Windows, Xbox + case strings.Contains(ua, "windows ") || strings.Contains(ua, "microsoft-cryptoapi"): + u.evalWindows(ua) + + // Kindle + case strings.Contains(ua, "kindle/") || amazonFireFingerprint.MatchString(agentPlatform): + u.OS.Platform = PlatformLinux + u.OS.Name = OSKindle + + // Linux (broader attempt) + case strings.Contains(ua, "linux"): + u.evalLinux(ua, agentPlatform) + + // WebOS (non-linux flagged) + case strings.Contains(ua, "webos") || strings.Contains(ua, "hpwos"): + u.OS.Platform = PlatformLinux + u.OS.Name = OSWebOS + + // Nintendo + case strings.Contains(ua, "nintendo"): + u.OS.Platform = PlatformNintendo + u.OS.Name = OSNintendo + + // Playstation + case strings.Contains(ua, "playstation") || strings.Contains(ua, "vita") || strings.Contains(ua, "psp"): + u.OS.Platform = PlatformPlaystation + u.OS.Name = OSPlaystation + + // Android + case strings.Contains(ua, "android"): + u.evalLinux(ua, agentPlatform) + + // Apple CFNetwork + case strings.Contains(ua, "cfnetwork") && strings.Contains(ua, "darwin"): + u.evalMacintosh(ua) + + default: + u.OS.Platform = PlatformUnknown + u.OS.Name = OSUnknown + } + } + + return u.isBot() +} + +func (u *UserAgent) isBot() bool { + + if u.OS.Platform == PlatformBot || u.OS.Name == OSBot { + u.DeviceType = DeviceComputer + return true + } + + if u.Browser.Name >= BrowserBot && u.Browser.Name <= BrowserYahooBot { + u.OS.Platform = PlatformBot + u.OS.Name = OSBot + u.DeviceType = DeviceComputer + return true + } + + return false +} + +// evalLinux returns the `Platform`, `OSName` and Version of UAs with +// 'linux' listed as their platform. +func (u *UserAgent) evalLinux(ua string, agentPlatform string) { + + switch { + // Kindle Fire + case strings.Contains(ua, "kindle") || amazonFireFingerprint.MatchString(agentPlatform): + // get the version of Android if available, though we don't call this OSAndroid + u.OS.Platform = PlatformLinux + u.OS.Name = OSKindle + u.OS.Version.findVersionNumber(agentPlatform, "android ") + + // Android, Kindle Fire + case strings.Contains(ua, "android") || strings.Contains(ua, "googletv"): + // Android + u.OS.Platform = PlatformLinux + u.OS.Name = OSAndroid + u.OS.Version.findVersionNumber(agentPlatform, "android ") + + // ChromeOS + case strings.Contains(ua, "cros"): + u.OS.Platform = PlatformLinux + u.OS.Name = OSChromeOS + + // WebOS + case strings.Contains(ua, "webos") || strings.Contains(ua, "hpwos"): + u.OS.Platform = PlatformLinux + u.OS.Name = OSWebOS + + // Linux, "Linux-like" + case strings.Contains(ua, "x11") || strings.Contains(ua, "bsd") || strings.Contains(ua, "suse") || strings.Contains(ua, "debian") || strings.Contains(ua, "ubuntu"): + u.OS.Platform = PlatformLinux + u.OS.Name = OSLinux + + default: + u.OS.Platform = PlatformLinux + u.OS.Name = OSLinux + } +} + +// evaliOS returns the `Platform`, `OSName` and Version of UAs with +// 'ipad' or 'iphone' listed as their platform. +func (u *UserAgent) evaliOS(uaPlatform string, agentPlatform string) { + + switch uaPlatform { + // iPhone + case "iphone": + u.OS.Platform = PlatformiPhone + u.OS.Name = OSiOS + u.OS.getiOSVersion(agentPlatform) + + // iPad + case "ipad": + u.OS.Platform = PlatformiPad + u.OS.Name = OSiOS + u.OS.getiOSVersion(agentPlatform) + + // iPod + case "ipod touch", "ipod": + u.OS.Platform = PlatformiPod + u.OS.Name = OSiOS + u.OS.getiOSVersion(agentPlatform) + + default: + u.OS.Platform = PlatformiPad + u.OS.Name = OSUnknown + } +} + +func (u *UserAgent) evalWindowsPhone(agentPlatform string) { + u.OS.Platform = PlatformWindowsPhone + + if u.OS.Version.findVersionNumber(agentPlatform, "windows phone os ") || u.OS.Version.findVersionNumber(agentPlatform, "windows phone ") { + u.OS.Name = OSWindowsPhone + } else { + u.OS.Name = OSUnknown + } +} + +func (u *UserAgent) evalWindows(ua string) { + + switch { + //Xbox -- it reads just like Windows + case strings.Contains(ua, "xbox"): + u.OS.Platform = PlatformXbox + u.OS.Name = OSXbox + if !u.OS.Version.findVersionNumber(ua, "windows nt ") { + u.OS.Version.Major = 6 + u.OS.Version.Minor = 0 + u.OS.Version.Patch = 0 + } + + // No windows version + case !strings.Contains(ua, "windows "): + u.OS.Platform = PlatformWindows + u.OS.Name = OSUnknown + + case strings.Contains(ua, "windows nt ") && u.OS.Version.findVersionNumber(ua, "windows nt "): + u.OS.Platform = PlatformWindows + u.OS.Name = OSWindows + + case strings.Contains(ua, "windows xp"): + u.OS.Platform = PlatformWindows + u.OS.Name = OSWindows + u.OS.Version.Major = 5 + u.OS.Version.Minor = 1 + u.OS.Version.Patch = 0 + + default: + u.OS.Platform = PlatformWindows + u.OS.Name = OSUnknown + + } +} + +func (u *UserAgent) evalMacintosh(uaPlatformGroup string) { + u.OS.Platform = PlatformMac + if i := strings.Index(uaPlatformGroup, "os x 10"); i != -1 { + u.OS.Name = OSMacOSX + u.OS.Version.parse(uaPlatformGroup[i+5:]) + + return + } + u.OS.Name = OSUnknown +} + +func (v *Version) findVersionNumber(s string, m string) bool { + if ind := strings.Index(s, m); ind != -1 { + return v.parse(s[ind+len(m):]) + } + return false +} + +// getiOSVersion accepts the platform portion of a UA string and returns +// a Version. +func (o *OS) getiOSVersion(uaPlatformGroup string) { + if i := strings.Index(uaPlatformGroup, "cpu iphone os "); i != -1 { + o.Version.parse(uaPlatformGroup[i+14:]) + return + } + + if i := strings.Index(uaPlatformGroup, "cpu os "); i != -1 { + o.Version.parse(uaPlatformGroup[i+7:]) + return + } + + o.Version.parse(uaPlatformGroup) +} + +// strToInt simply accepts a string and returns a `int`, +// with '0' being default. +func strToInt(str string) int { + i, _ := strconv.Atoi(str) + return i +} + +// strToVer accepts a string and returns a Version, +// with {0, 0, 0} being default. +func (v *Version) parse(str string) bool { + if len(str) == 0 || str[0] < '0' || str[0] > '9' { + return false + } + for i := 0; i < 3; i++ { + empty := true + val := 0 + l := len(str) - 1 + + for k, c := range str { + if c >= '0' && c <= '9' { + if empty { + val = int(c) - 48 + empty = false + if k == l { + str = str[:0] + } + continue + } + + if val == 0 { + if c == '0' { + if k == l { + str = str[:0] + } + continue + } + str = str[k:] + break + } + + val = 10*val + int(c) - 48 + if k == l { + str = str[:0] + } + continue + } + str = str[k+1:] + break + } + + switch i { + case 0: + v.Major = val + + case 1: + v.Minor = val + + case 2: + v.Patch = val + } + } + return true +} diff --git a/vendor/github.com/avct/uasurfer/uasurfer.go b/vendor/github.com/avct/uasurfer/uasurfer.go new file mode 100644 index 000000000..15aac6d40 --- /dev/null +++ b/vendor/github.com/avct/uasurfer/uasurfer.go @@ -0,0 +1,227 @@ +// Package uasurfer provides fast and reliable abstraction +// of HTTP User-Agent strings. The philosophy is to identify +// technologies that holds >1% market share, and to avoid +// expending resources and accuracy on guessing at esoteric UA +// strings. +package uasurfer + +import "strings" + +//go:generate stringer -type=DeviceType,BrowserName,OSName,Platform -output=const_string.go + +// DeviceType (int) returns a constant. +type DeviceType int + +// A complete list of supported devices in the +// form of constants. +const ( + DeviceUnknown DeviceType = iota + DeviceComputer + DeviceTablet + DevicePhone + DeviceConsole + DeviceWearable + DeviceTV +) + +// BrowserName (int) returns a constant. +type BrowserName int + +// A complete list of supported web browsers in the +// form of constants. +const ( + BrowserUnknown BrowserName = iota + BrowserChrome + BrowserIE + BrowserSafari + BrowserFirefox + BrowserAndroid + BrowserOpera + BrowserBlackberry + BrowserUCBrowser + BrowserSilk + BrowserNokia + BrowserNetFront + BrowserQQ + BrowserMaxthon + BrowserSogouExplorer + BrowserSpotify + BrowserBot // Bot list begins here + BrowserAppleBot + BrowserBaiduBot + BrowserBingBot + BrowserDuckDuckGoBot + BrowserFacebookBot + BrowserGoogleBot + BrowserLinkedInBot + BrowserMsnBot + BrowserPingdomBot + BrowserTwitterBot + BrowserYandexBot + BrowserYahooBot // Bot list ends here +) + +// OSName (int) returns a constant. +type OSName int + +// A complete list of supported OSes in the +// form of constants. For handling particular versions +// of operating systems (e.g. Windows 2000), see +// the README.md file. +const ( + OSUnknown OSName = iota + OSWindowsPhone + OSWindows + OSMacOSX + OSiOS + OSAndroid + OSBlackberry + OSChromeOS + OSKindle + OSWebOS + OSLinux + OSPlaystation + OSXbox + OSNintendo + OSBot +) + +// Platform (int) returns a constant. +type Platform int + +// A complete list of supported platforms in the +// form of constants. Many OSes report their +// true platform, such as Android OS being Linux +// platform. +const ( + PlatformUnknown Platform = iota + PlatformWindows + PlatformMac + PlatformLinux + PlatformiPad + PlatformiPhone + PlatformiPod + PlatformBlackberry + PlatformWindowsPhone + PlatformPlaystation + PlatformXbox + PlatformNintendo + PlatformBot +) + +type Version struct { + Major int + Minor int + Patch int +} + +func (v Version) Less(c Version) bool { + if v.Major < c.Major { + return true + } + + if v.Major > c.Major { + return false + } + + if v.Minor < c.Minor { + return true + } + + if v.Minor > c.Minor { + return false + } + + return v.Patch < c.Patch +} + +type UserAgent struct { + Browser Browser + OS OS + DeviceType DeviceType +} + +type Browser struct { + Name BrowserName + Version Version +} + +type OS struct { + Platform Platform + Name OSName + Version Version +} + +// Reset resets the UserAgent to it's zero value +func (ua *UserAgent) Reset() { + ua.Browser = Browser{} + ua.OS = OS{} + ua.DeviceType = DeviceUnknown +} + +// Parse accepts a raw user agent (string) and returns the UserAgent. +func Parse(ua string) *UserAgent { + dest := new(UserAgent) + parse(ua, dest) + return dest +} + +// ParseUserAgent is the same as Parse, but populates the supplied UserAgent. +// It is the caller's responsibility to call Reset() on the UserAgent before +// passing it to this function. +func ParseUserAgent(ua string, dest *UserAgent) { + parse(ua, dest) +} + +func parse(ua string, dest *UserAgent) { + ua = normalise(ua) + switch { + case len(ua) == 0: + dest.OS.Platform = PlatformUnknown + dest.OS.Name = OSUnknown + dest.Browser.Name = BrowserUnknown + dest.DeviceType = DeviceUnknown + + // stop on on first case returning true + case dest.evalOS(ua): + case dest.evalBrowserName(ua): + default: + dest.evalBrowserVersion(ua) + dest.evalDevice(ua) + } +} + +// normalise normalises the user supplied agent string so that +// we can more easily parse it. +func normalise(ua string) string { + if len(ua) <= 1024 { + var buf [1024]byte + ascii := copyLower(buf[:len(ua)], ua) + if !ascii { + // Fall back for non ascii characters + return strings.ToLower(ua) + } + return string(buf[:len(ua)]) + } + // Fallback for unusually long strings + return strings.ToLower(ua) +} + +// copyLower copies a lowercase version of s to b. It assumes s contains only single byte characters +// and will panic if b is nil or is not long enough to contain all the bytes from s. +// It returns early with false if any characters were non ascii. +func copyLower(b []byte, s string) bool { + for j := 0; j < len(s); j++ { + c := s[j] + if c > 127 { + return false + } + + if 'A' <= c && c <= 'Z' { + c += 'a' - 'A' + } + + b[j] = c + } + return true +} diff --git a/vendor/github.com/avct/uasurfer/uasurfer_test.go b/vendor/github.com/avct/uasurfer/uasurfer_test.go new file mode 100644 index 000000000..7a4688bb2 --- /dev/null +++ b/vendor/github.com/avct/uasurfer/uasurfer_test.go @@ -0,0 +1,1074 @@ +package uasurfer + +import "testing" + +var testUAVars = []struct { + UA string + UserAgent +}{ + // Empty + {"", + UserAgent{}}, + + // Single char + {"a", + UserAgent{}}, + + // Some random string + {"some random string", + UserAgent{}}, + + // Potentially malformed ua + {")(", + UserAgent{}}, + + // iPhone + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/546.10 (KHTML, like Gecko) Version/6.0 Mobile/7E18WD Safari/8536.25", + UserAgent{ + Browser{BrowserSafari, Version{6, 0, 0}}, OS{PlatformiPhone, OSiOS, Version{7, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A405 Safari/600.1.4", + UserAgent{ + Browser{BrowserSafari, Version{8, 0, 0}}, OS{PlatformiPhone, OSiOS, Version{8, 0, 2}}, DevicePhone}}, + + // iPad + {"Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314 Safari/531.21.10", + UserAgent{ + Browser{BrowserSafari, Version{4, 0, 4}}, OS{PlatformiPad, OSiOS, Version{3, 2, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPad; CPU OS 9_0 like Mac OS X) AppleWebKit/601.1.17 (KHTML, like Gecko) Version/8.0 Mobile/13A175 Safari/600.1.4", + UserAgent{ + Browser{BrowserSafari, Version{8, 0, 0}}, OS{PlatformiPad, OSiOS, Version{9, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.32 (KHTML, like Gecko) Version/10.0 Mobile/14A5261v Safari/602.1", + UserAgent{ + Browser{BrowserSafari, Version{10, 0, 0}}, OS{PlatformiPhone, OSiOS, Version{10, 0, 0}}, DevicePhone}}, + + // Chrome + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{43, 0, 2357}}, OS{PlatformMac, OSMacOSX, Version{10, 10, 4}}, DeviceComputer}}, + + {"Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/534.48.3", + UserAgent{ + Browser{BrowserChrome, Version{19, 0, 1084}}, OS{PlatformiPhone, OSiOS, Version{5, 1, 1}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; Android 6.0; Nexus 5X Build/MDB08L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{46, 0, 2490}}, OS{PlatformLinux, OSAndroid, Version{6, 0, 0}}, DevicePhone}}, + + // Chromium (Chrome) + {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Ubuntu/11.10 Chromium/18.0.1025.142 Chrome/18.0.1025.142 Safari/535.19", + UserAgent{ + Browser{BrowserChrome, Version{18, 0, 1025}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{45, 0, 2454}}, OS{PlatformMac, OSMacOSX, Version{10, 11, 0}}, DeviceComputer}}, + + //TODO: refactor "getVersion()" to handle this device/chrome version douchebaggery + // {"Mozilla/5.0 (Linux; Android 4.4.2; en-gb; SAMSUNG SM-G800F Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.6 Chrome/28.0.1500.94 Mobile Safari/537.36", + // UserAgent{ + // Browser{BrowserChrome, Version{28,0,1500}, OS{PlatformLinux, OSAndroid, Version{4,4,2}}, DevicePhone}}, + + // Safari + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12", + UserAgent{ + Browser{BrowserSafari, Version{8, 0, 7}}, OS{PlatformMac, OSMacOSX, Version{10, 10, 4}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.12", + UserAgent{ + Browser{BrowserSafari, Version{3, 2, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 5, 5}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12) AppleWebKit/602.1.32 (KHTML, like Gecko) Version/10.0 Safari/602.1.32", // macOS Sierra dev beta + UserAgent{ + Browser{BrowserSafari, Version{10, 0, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 12, 0}}, DeviceComputer}}, + + // Firefox + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/600.1.4", + UserAgent{ + Browser{BrowserFirefox, Version{1, 0, 0}}, OS{PlatformiPhone, OSiOS, Version{8, 3, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Android 4.4; Tablet; rv:41.0) Gecko/41.0 Firefox/41.0", + UserAgent{ + Browser{BrowserFirefox, Version{41, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 4, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (Android; Mobile; rv:40.0) Gecko/40.0 Firefox/40.0", + UserAgent{ + Browser{BrowserFirefox, Version{40, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0", + UserAgent{ + Browser{BrowserFirefox, Version{38, 0, 0}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceComputer}}, + + // Silk + {"Mozilla/5.0 (Linux; U; Android 4.4.3; de-de; KFTHWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.47 like Chrome/37.0.2026.117 Safari/537.36", + UserAgent{ + Browser{BrowserSilk, Version{3, 47, 0}}, OS{PlatformLinux, OSKindle, Version{4, 4, 3}}, DeviceTablet}}, + + {"Mozilla/5.0 (Linux; U; en-us; KFJWI Build/IMM76D) AppleWebKit/535.19 (KHTML like Gecko) Silk/2.4 Safari/535.19 Silk-Acceleratedtrue", + UserAgent{ + Browser{BrowserSilk, Version{2, 4, 0}}, OS{PlatformLinux, OSKindle, Version{0, 0, 0}}, DeviceTablet}}, + + // Opera + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 OPR/18.0.1284.68", + UserAgent{ + Browser{BrowserOpera, Version{18, 0, 1284}}, OS{PlatformWindows, OSWindows, Version{6, 1, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) OPiOS/10.2.0.93022 Mobile/12H143 Safari/9537.53", + UserAgent{ + Browser{BrowserOpera, Version{10, 2, 0}}, OS{PlatformiPhone, OSiOS, Version{8, 4, 0}}, DevicePhone}}, + + // Internet Explorer -- https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.123", + UserAgent{ + Browser{BrowserIE, Version{12, 123, 0}}, OS{PlatformWindows, OSWindows, Version{10, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)", + UserAgent{ + Browser{BrowserIE, Version{10, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 2, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C; rv:11.0) like Gecko", + UserAgent{ + Browser{BrowserIE, Version{11, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 3, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; DEVICE INFO) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Mobile Safari/537.36 Edge/12.123", + UserAgent{ + Browser{BrowserIE, Version{12, 123, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{10, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 520) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537", + UserAgent{ + Browser{BrowserIE, Version{11, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{8, 1, 0}}, DevicePhone}}, + + {"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1; .NET CLR 1.1.4322; .NET CLR 1.0.3705; .NET CLR 2.0.50727)", + UserAgent{ + Browser{BrowserIE, Version{5, 0, 1}}, OS{PlatformWindows, OSWindows, Version{5, 0, 0}}, DeviceComputer}}, + + {"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/4.0; GTB6.4; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; OfficeLiveConnector.1.3; OfficeLivePatch.0.0; .NET CLR 1.1.4322)", + UserAgent{ + Browser{BrowserIE, Version{7, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 1, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; ARM; Trident/6.0; Touch)", //Windows Surface RT tablet + UserAgent{ + Browser{BrowserIE, Version{10, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 2, 0}}, DeviceTablet}}, + + // UC Browser + {"Mozilla/5.0 (Linux; U; Android 2.3.4; en-US; MT11i Build/4.0.2.A.0.62) AppleWebKit/534.31 (KHTML, like Gecko) UCBrowser/9.0.1.275 U3/0.8.0 Mobile Safari/534.31", + UserAgent{ + Browser{BrowserUCBrowser, Version{9, 0, 1}}, OS{PlatformLinux, OSAndroid, Version{2, 3, 4}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 4.0.4; en-US; Micromax P255 Build/IMM76D) AppleWebKit/534.31 (KHTML, like Gecko) UCBrowser/9.2.0.308 U3/0.8.0 Mobile Safari/534.31", + UserAgent{ + Browser{BrowserUCBrowser, Version{9, 2, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 0, 4}}, DevicePhone}}, + + {"UCWEB/2.0 (Java; U; MIDP-2.0; en-US; MicromaxQ5) U2/1.0.0 UCBrowser/9.4.0.342 U2/1.0.0 Mobile", + UserAgent{ + Browser{BrowserUCBrowser, Version{9, 4, 0}}, OS{PlatformUnknown, OSUnknown, Version{0, 0, 0}}, DevicePhone}}, + + // Nokia Browser + // {"Mozilla/5.0 (Series40; Nokia501/14.0.4/java_runtime_version=Nokia_Asha_1_2; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/4.0.0.0.45", + // UserAgent{ + // Browser{BrowserUnknown, Version{4,0,0}}, OS{PlatformUnknown, OSUnknown, Version{0,0,0}}, DevicePhone}}, + + // {"Mozilla/5.0 (Symbian/3; Series60/5.3 NokiaN8-00/111.040.1511; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/535.1 (KHTML, like Gecko) NokiaBrowser/8.3.1.4 Mobile Safari/535.1", + // UserAgent{ + // Browser{BrowserUnknown, Version{8,0,0}}, OS{PlatformUnknown, OSUnknown, Version{0,0,0}}, DevicePhone}}, + + // {"NokiaN97/21.1.107 (SymbianOS/9.4; Series60/5.0 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebkit/525 (KHTML, like Gecko) BrowserNG/7.1.4", + // BrowserUnknown, Version{7,0,0}}, OS{PlatformUnknown, OSUnknown, Version{0,0,0}}, DevicePhone}}, + + // ChromeOS + {"Mozilla/5.0 (X11; U; CrOS i686 9.10.0; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.253.0 Safari/532.5", + UserAgent{ + Browser{BrowserChrome, Version{4, 0, 253}}, OS{PlatformLinux, OSChromeOS, Version{0, 0, 0}}, DeviceComputer}}, + + // iPod, iPod Touch + {"mozilla/5.0 (ipod touch; cpu iphone os 9_3_3 like mac os x) applewebkit/601.1.46 (khtml, like gecko) version/9.0 mobile/13g34 safari/601.1", + UserAgent{ + Browser{BrowserSafari, Version{9, 0, 0}}, OS{PlatformiPod, OSiOS, Version{9, 3, 3}}, DeviceTablet}}, + + {"mozilla/5.0 (ipod; cpu iphone os 6_1_6 like mac os x) applewebkit/536.26 (khtml, like gecko) version/6.0 mobile/10b500 safari/8536.25", + UserAgent{ + Browser{BrowserSafari, Version{6, 0, 0}}, OS{PlatformiPod, OSiOS, Version{6, 1, 6}}, DeviceTablet}}, + + // WebOS + {"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformLinux, OSWebOS, Version{0, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (webOS/1.4.1.1; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pre/1.0", + UserAgent{ + Browser{BrowserUnknown, Version{1, 0, 0}}, OS{PlatformLinux, OSWebOS, Version{0, 0, 0}}, DevicePhone}}, + + // Android WebView (Android <= 4.3) + {"Mozilla/5.0 (Linux; U; Android 2.2; en-us; DROID2 GLOBAL Build/S273) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 2, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 4.0.3; de-ch; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari53/4.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 0, 3}}, DevicePhone}}, + + // BlackBerry + {"Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML, like Gecko) Version/7.2.1.0 Safari/536.2+", + UserAgent{ + Browser{BrowserBlackberry, Version{7, 2, 1}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (BB10; Kbd) AppleWebKit/537.35+ (KHTML, like Gecko) Version/10.2.1.1925 Mobile Safari/537.35+", + UserAgent{ + Browser{BrowserBlackberry, Version{10, 2, 1}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0) BlackBerry8703e/4.1.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/104", + UserAgent{ + Browser{BrowserBlackberry, Version{0, 0, 0}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DevicePhone}}, + + // Windows Phone + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 625; ANZ941)", + UserAgent{ + Browser{BrowserIE, Version{10, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{8, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 900)", + UserAgent{ + Browser{BrowserIE, Version{9, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{7, 5, 0}}, DevicePhone}}, + + // Kindle eReader + {"Mozilla/5.0 (Linux; U; en-US) AppleWebKit/528.5+ (KHTML, like Gecko, Safari/528.5+) Version/4.0 Kindle/3.0 (screen 600×800; rotate)", + UserAgent{ + Browser{BrowserUnknown, Version{4, 0, 0}}, OS{PlatformLinux, OSKindle, Version{0, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (X11; U; Linux armv7l like Android; en-us) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/533.2+ Kindle/3.0+", + UserAgent{ + Browser{BrowserUnknown, Version{5, 0, 0}}, OS{PlatformLinux, OSKindle, Version{0, 0, 0}}, DeviceTablet}}, + + // Amazon Fire + {"Mozilla/5.0 (Linux; U; Android 4.4.3; de-de; KFTHWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.67 like Chrome/39.0.2171.93 Safari/537.36", + UserAgent{ + Browser{BrowserSilk, Version{3, 67, 0}}, OS{PlatformLinux, OSKindle, Version{4, 4, 3}}, DeviceTablet}}, // Fire tablet + + {"Mozilla/5.0 (Linux; U; Android 4.2.2; enus; KFTHWI Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.22 like Chrome/34.0.1847.137 Mobile Safari/537.36", + UserAgent{ + Browser{BrowserSilk, Version{3, 22, 0}}, OS{PlatformLinux, OSKindle, Version{4, 2, 2}}, DeviceTablet}}, // Fire tablet, but with "Mobile" + + {"Mozilla/5.0 (Linux; Android 4.4.4; SD4930UR Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/34.0.0.0 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/35.0.0.48.273;]", + UserAgent{ + Browser{BrowserChrome, Version{34, 0, 0}}, OS{PlatformLinux, OSKindle, Version{4, 4, 4}}, DevicePhone}}, // Facebook app on Fire Phone + + {"mozilla/5.0 (linux; android 4.4.3; kfthwi build/ktu84m) applewebkit/537.36 (khtml, like gecko) version/4.0 chrome/34.0.0.0 safari/537.36 [pinterest/android]", + UserAgent{ + Browser{BrowserChrome, Version{34, 0, 0}}, OS{PlatformLinux, OSKindle, Version{4, 4, 3}}, DeviceTablet}}, // Fire tablet running pinterest + + // extra logic to identify phone when using silk has not been added + // {"Mozilla/5.0 (Linux; Android 4.4.4; SD4930UR Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.67 like Chrome/39.0.2171.93 Mobile Safari/537.36", + // UserAgent{ + // Browser{BrowserSilk, Version{3,0,0}}, OS{PlatformLinux, OSKindle, Version{4,0,0}}, DevicePhone}}, // Silk on Fire Phone + + // Nintendo + {"Opera/9.30 (Nintendo Wii; U; ; 2047-7; fr)", + UserAgent{ + Browser{BrowserOpera, Version{9, 30, 0}}, OS{PlatformNintendo, OSNintendo, Version{0, 0, 0}}, DeviceConsole}}, + + {"Mozilla/5.0 (Nintendo WiiU) AppleWebKit/534.52 (KHTML, like Gecko) NX/2.1.0.8.21 NintendoBrowser/1.0.0.7494.US", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformNintendo, OSNintendo, Version{0, 0, 0}}, DeviceConsole}}, + + // Xbox + {"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; Xbox)", //Xbox 360 + UserAgent{ + Browser{BrowserIE, Version{9, 0, 0}}, OS{PlatformXbox, OSXbox, Version{6, 1, 0}}, DeviceConsole}}, + + // Playstation + {"Mozilla/5.0 (PlayStation 4 4.50) AppleWebKit/601.2 (KHTML, like Gecko)", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformPlaystation, OSPlaystation, Version{0, 0, 0}}, DeviceConsole}}, + + {"Mozilla/5.0 (Playstation Vita 1.61) AppleWebKit/531.22.8 (KHTML, like Gecko) Silk/3.2", + UserAgent{ + Browser{BrowserSilk, Version{3, 2, 0}}, OS{PlatformPlaystation, OSPlaystation, Version{0, 0, 0}}, DeviceConsole}}, + + // Smart TVs and TV dongles + {"Mozilla/5.0 (CrKey armv7l 1.4.15250) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.0 Safari/537.36", // Chromecast + UserAgent{ + Browser{BrowserChrome, Version{31, 0, 1650}}, OS{PlatformUnknown, OSUnknown, Version{0, 0, 0}}, DeviceTV}}, + + {"Mozilla/5.0 (Linux; GoogleTV 3.2; VAP430 Build/MASTER) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.77 Safari/534.24", // Google TV + UserAgent{ + Browser{BrowserChrome, Version{11, 0, 696}}, OS{PlatformLinux, OSAndroid, Version{0, 0, 0}}, DeviceTV}}, + + {"Mozilla/5.0 (Linux; Android 5.0; ADT-1 Build/LPX13D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.89 Mobile Safari/537.36", // Android TV + UserAgent{ + Browser{BrowserChrome, Version{40, 0, 2214}}, OS{PlatformLinux, OSAndroid, Version{5, 0, 0}}, DeviceTV}}, + + {"Mozilla/5.0 (Linux; Android 4.2.2; AFTB Build/JDQ39) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.173 Mobile Safari/537.22", // Amazon Fire + UserAgent{ + Browser{BrowserChrome, Version{25, 0, 1364}}, OS{PlatformLinux, OSAndroid, Version{4, 2, 2}}, DeviceTV}}, + + {"Mozilla/5.0 (Unknown; Linux armv7l) AppleWebKit/537.1+ (KHTML, like Gecko) Safari/537.1+ LG Browser/6.00.00(+mouse+3D+SCREEN+TUNER; LGE; GLOBAL-PLAT5; 03.07.01; 0x00000001;); LG NetCast.TV-2013/03.17.01 (LG, GLOBAL-PLAT4, wired)", // LG TV + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceTV}}, + + {"Mozilla/5.0 (X11; FreeBSD; U; Viera; de-DE) AppleWebKit/537.11 (KHTML, like Gecko) Viera/3.10.0 Chrome/23.0.1271.97 Safari/537.11", // Panasonic Viera + UserAgent{ + Browser{BrowserChrome, Version{23, 0, 1271}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceTV}}, + + // TODO: not catching "browser/" and reporting as safari -- ua string not being fully checked? + // {"Mozilla/5.0 (DTV) AppleWebKit/531.2+ (KHTML, like Gecko) Espial/6.1.5 AQUOSBrowser/2.0 (US01DTV;V;0001;0001)", // Sharp Aquos + // BrowserUnknown, Version{0,0,0}}, OS{PlatformUnknown, OSUnknown, Version{0,0,0}}, DeviceTV}}, + + {"Roku/DVP-5.2 (025.02E03197A)", // Roku + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformUnknown, OSUnknown, Version{0, 0, 0}}, DeviceTV}}, + + {"mozilla/5.0 (smart-tv; linux; tizen 2.3) applewebkit/538.1 (khtml, like gecko) samsungbrowser/1.0 tv safari/538.1", // Samsung SmartTV + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceTV}}, + + {"mozilla/5.0 (linux; u) applewebkit/537.36 (khtml, like gecko) version/4.0 mobile safari/537.36 smarttv/6.0 (netcast)", + UserAgent{ + Browser{BrowserUnknown, Version{4, 0, 0}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceTV}}, + + // Google search app (GSA) for iOS -- it's Safari in disguise as of v6 + {"Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/6.0.51363 Mobile/12F69 Safari/600.1.4", + UserAgent{ + Browser{BrowserSafari, Version{8, 3, 0}}, OS{PlatformiPad, OSiOS, Version{8, 3, 0}}, DeviceTablet}}, + + // Spotify (applicable for advertising applications) + {"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Spotify/1.0.9.133 Safari/537.36", + UserAgent{ + Browser{BrowserSpotify, Version{1, 0, 9}}, OS{PlatformWindows, OSWindows, Version{5, 1, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Spotify/1.0.9.133 Safari/537.36", + UserAgent{ + Browser{BrowserSpotify, Version{1, 0, 9}}, OS{PlatformMac, OSMacOSX, Version{10, 10, 2}}, DeviceComputer}}, + + // OCSP fetchers + {"Microsoft-CryptoAPI/10.0", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformWindows, OSUnknown, Version{0, 0, 0}}, DeviceComputer}}, + {"trustd (unknown version) CFNetwork/811.7.2 Darwin/16.7.0 (x86_64)", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformMac, OSUnknown, Version{0, 0, 0}}, DeviceComputer}}, + {"ocspd (unknown version) CFNetwork/520.5.3 Darwin/11.4.2 (x86_64)(MacBookAir5%2C2)", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformMac, OSUnknown, Version{0, 0, 0}}, DeviceComputer}}, + // Bots + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/8.0.2 Safari/600.2.5 (Applebot/0.1; +http://www.apple.com/go/applebot)", + UserAgent{ + Browser{BrowserAppleBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{10, 10, 1}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)", + UserAgent{ + Browser{BrowserBaiduBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", + UserAgent{ + Browser{BrowserBingBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)", + UserAgent{ + Browser{BrowserDuckDuckGoBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)", + UserAgent{ + Browser{BrowserFacebookBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"Facebot/1.0", + UserAgent{ + Browser{BrowserFacebookBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", + UserAgent{ + Browser{BrowserGoogleBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"LinkedInBot/1.0 (compatible; Mozilla/5.0; Jakarta Commons-HttpClient/3.1 +http://www.linkedin.com)", + UserAgent{ + Browser{BrowserLinkedInBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"msnbot/2.0b (+http://search.msn.com/msnbot.htm)", + UserAgent{ + Browser{BrowserMsnBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"Pingdom.com_bot_version_1.4_(http://www.pingdom.com/)", + UserAgent{ + Browser{BrowserPingdomBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"Twitterbot/1.0", + UserAgent{ + Browser{BrowserTwitterBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)", + UserAgent{ + Browser{BrowserYandexBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)", + UserAgent{ + Browser{BrowserYahooBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + // {"Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", + // BrowserBot, Version{0,0,0}}, OS{PlatformBot, OSBot, Version{6,0,0}}, DeviceComputer}}, + + {"mozilla/5.0 (unknown; linux x86_64) applewebkit/538.1 (khtml, like gecko) phantomjs/2.1.1 safari/538.1", + UserAgent{ + Browser{BrowserBot, Version{0, 0, 0}}, OS{PlatformBot, OSBot, Version{0, 0, 0}}, DeviceComputer}}, + + // Unknown or partially handled + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; en-US; rv:1.9.1b3pre) Gecko/20090223 SeaMonkey/2.0a3", //Seamonkey (~FF) + UserAgent{ + Browser{BrowserFirefox, Version{0, 0, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 4, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en; rv:1.9.0.8pre) Gecko/2009022800 Camino/2.0b3pre", //Camino (~FF) + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 5, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Mobile; rv:26.0) Gecko/26.0 Firefox/26.0", //firefox OS + UserAgent{ + Browser{BrowserFirefox, Version{26, 0, 0}}, OS{PlatformUnknown, OSUnknown, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19", //chrome for android having requested desktop site + UserAgent{ + Browser{BrowserChrome, Version{18, 0, 1025}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceComputer}}, + + {"Opera/9.80 (S60; SymbOS; Opera Mobi/352; U; de) Presto/2.4.15 Version/10.00", + UserAgent{ + Browser{BrowserOpera, Version{10, 0, 0}}, OS{PlatformUnknown, OSUnknown, Version{0, 0, 0}}, DevicePhone}}, + + // BrowserQQ + // {"Mozilla/5.0 (Windows NT 6.2; WOW64; Trident/7.0; Touch; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 2.0.50727; .NET CLR 3.0.30729; InfoPath.3; Tablet PC 2.0; QQBrowser/7.6.21433.400; rv:11.0) like Gecko", + // UserAgent{ + // Browser{BrowserQQ, Version{7,0,0}}, OS{PlatformWindows, OSWindows, Version{8,0,0}}, DeviceTablet}}, + + // {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36 QQBrowser/9.0.2191.400", + // UserAgent{ + // Browser{BrowserQQ, Version{9,0,0}}, OS{PlatformWindows, OSWindows, Version{7,0,0}}, DeviceComputer}}, + + // ANDROID TESTS + + {"Mozilla/5.0 (Linux; U; Android 1.0; en-us; dream) AppleWebKit/525.10+ (KHTML,like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{1, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.0; en-us; generic) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{1, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.0.3; de-de; A80KSC Build/ECLAIR) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{1, 0, 3}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.5; en-gb; T-Mobile G1 Build/CRC1) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1", + UserAgent{ + Browser{BrowserAndroid, Version{3, 1, 2}}, OS{PlatformLinux, OSAndroid, Version{1, 5, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.5; es-; FBW1_4 Build/MASTER) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{1, 5, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux U; Android 1.5 en-us hero) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{1, 5, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.5; en-us; Opus One Build/RBE.00.00) AppleWebKit/528.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile Safari/525.20.1", + UserAgent{ + Browser{BrowserAndroid, Version{3, 1, 1}}, OS{PlatformLinux, OSAndroid, Version{1, 5, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.6; ar-us; SonyEricssonX10i Build/R2BA026) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1", + UserAgent{ + Browser{BrowserAndroid, Version{3, 1, 2}}, OS{PlatformLinux, OSAndroid, Version{1, 6, 0}}, DevicePhone}}, + + // TODO: support names of Android OS? + //{"Mozilla/5.0 (Linux; U; Android Donut; de-de; HTC Tattoo 1.52.161.1 Build/Donut) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1", + // UserAgent{ + // Browser{BrowserAndroid, Version{3, 1, 2}}, OS{PlatformLinux, OSAndroid, Version{1, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.6; en-gb; HTC Tattoo Build/DRC79) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{1, 6, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 1.6; ja-jp; Docomo HT-03A Build/DRD08) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{1, 6, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 1, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.1-update1; en-au; HTC_Desire_A8183 V1.16.841.1 Build/ERE27) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 1, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.1; en-us; generic) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{2, 1, 0}}, DevicePhone}}, + + // TODO support named versions of Android? + {"Mozilla/5.0 (Linux; U; Android Eclair; en-us; sholes) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", + UserAgent{ + Browser{BrowserAndroid, Version{3, 0, 4}}, OS{PlatformLinux, OSAndroid, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.2; en-sa; HTC_DesireHD_A9191 Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 2, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.2.1; en-gb; HTC_DesireZ_A7272 Build/FRG83D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 2, 1}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.3.3; en-us; Sensation_4G Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/5.0 Safari/533.16", + UserAgent{ + Browser{BrowserAndroid, Version{5, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 3, 3}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.3.5; ko-kr; SHW-M250S Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 3, 5}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 2.3.7; ja-jp; L-02D Build/GWK74) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{2, 3, 7}}, DevicePhone}}, + + // TODO: is tablet, not phone + {"Mozilla/5.0 (Linux; U; Android 3.0; xx-xx; Transformer TF101 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{3, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{3, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (Linux; U; Android 4.0.1; en-us; sdk Build/ICS_MR0) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 0, 1}}, DevicePhone}}, + + // TODO support "android-" version prefix + // However, can't find reference to this naming scheme in real-world UA gathering + // {"Mozilla/5.0 (Linux; U; Android-4.0.3; en-us; Galaxy Nexus Build/IML74K) AppleWebKit/535.7 (KHTML, like Gecko) CrMo/16.0.912.75 Mobile Safari/535.7", + // UserAgent{ + // Browser{BrowserChrome, Version{16,0,0}}, OS{PlatformLinux, OSAndroid, Version{4,0,0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Nexus S Build/JRO03E) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 1, 1}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 4.1; en-gb; Build/JRN84D) AppleWebKit/534.30 (KHTML like Gecko) Version/4.0 Mobile Safari/534.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 1, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 4.1.1; el-gr; MB525 Build/JRO03H; CyanogenMod-10) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 1, 1}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 4.1.1; fr-fr; MB525 Build/JRO03H; CyanogenMod-10) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 1, 1}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; U; Android 4.2; en-us; Nexus 10 Build/JVP15I) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 2, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (Linux; U; Android 4.2; ro-ro; LT18i Build/4.1.B.0.431) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + UserAgent{ + Browser{BrowserAndroid, Version{4, 0, 0}}, OS{PlatformLinux, OSAndroid, Version{4, 2, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JWR66D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.111 Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{27, 0, 1453}}, OS{PlatformLinux, OSAndroid, Version{4, 3, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (Linux; Android 4.4; Nexus 7 Build/KOT24) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.105 Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{30, 0, 1599}}, OS{PlatformLinux, OSAndroid, Version{4, 4, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (Linux; Android 4.4; Nexus 4 Build/KRT16E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.105 Mobile Safari", + UserAgent{ + Browser{BrowserChrome, Version{30, 0, 1599}}, OS{PlatformLinux, OSAndroid, Version{4, 4, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; Android 6.0.1; SM-G930V Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{52, 0, 2743}}, OS{PlatformLinux, OSAndroid, Version{6, 0, 1}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; Android 7.0; Nexus 5X Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{52, 0, 2743}}, OS{PlatformLinux, OSAndroid, Version{7, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Linux; Android 7.0; Nexus 6P Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/52.0.2743.98 Mobile Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{52, 0, 2743}}, OS{PlatformLinux, OSAndroid, Version{7, 0, 0}}, DevicePhone}}, + + // BLACKBERRY TESTS + + {"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0) BlackBerry8703e/4.1.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/104", + UserAgent{ + Browser{BrowserBlackberry, Version{0, 0, 0}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.1.0.4633 Mobile Safari/537.10+", + UserAgent{ + Browser{BrowserBlackberry, Version{10, 1, 0}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (BB10; Kbd) AppleWebKit/537.35+ (KHTML, like Gecko) Version/10.2.1.1925 Mobile Safari/537.35+", + UserAgent{ + Browser{BrowserBlackberry, Version{10, 2, 1}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (PlayBook; U; RIM Tablet OS 1.0.0; en-US) AppleWebKit/534.11 (KHTML, like Gecko) Version/7.1.0.7 Safari/534.11", + UserAgent{ + Browser{BrowserBlackberry, Version{7, 1, 0}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML, like Gecko) Version/7.2.1.0 Safari/536.2+", + UserAgent{ + Browser{BrowserBlackberry, Version{7, 2, 1}}, OS{PlatformBlackberry, OSBlackberry, Version{0, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (X11; U; CrOS i686 9.10.0; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.253.0 Safari/532.5", + UserAgent{ + Browser{BrowserChrome, Version{4, 0, 253}}, OS{PlatformLinux, OSChromeOS, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (X11; CrOS armv7l 5500.100.6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.120 Safari/537.36", + UserAgent{ + Browser{BrowserChrome, Version{34, 0, 1847}}, OS{PlatformLinux, OSChromeOS, Version{0, 0, 0}}, DeviceComputer}}, + + // {"Mozilla/5.0 (Mobile; rv:14.0) Gecko/14.0 Firefox/14.0", + // UserAgent{ + // Browser{BrowserFirefox, 14, OSFirefoxOS, 14}, DevicePhone}}, + + // {"Mozilla/5.0 (Mobile; rv:17.0) Gecko/17.0 Firefox/17.0", + // UserAgent{ + // Browser{BrowserFirefox, , OSFirefoxOS}, DevicePhone}}, + + // {"Mozilla/5.0 (Mobile; rv:18.1) Gecko/18.1 Firefox/18.1", + // UserAgent{ + // Browser{BrowserFirefox, , OSFirefoxOS}, DevicePhone}}, + + // {"Mozilla/5.0 (Tablet; rv:18.1) Gecko/18.1 Firefox/18.1", + // UserAgent{ + // Browser{BrowserFirefox, , OSFirefoxOS}, DevicePhone}}, + + // {"Mozilla/5.0 (Mobile; LG-D300; rv:18.1) Gecko/18.1 Firefox/18.1", + // UserAgent{ + // Browser{BrowserFirefox, , OSFirefoxOS}, DevicePhone}}, + + {"Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314 Safari/531.21.10", + UserAgent{ + Browser{BrowserSafari, Version{4, 0, 4}}, OS{PlatformiPad, OSiOS, Version{3, 2, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_0 like Mac OS X; en-us) AppleWebKit/532.9 (KHTML, like Gecko) Version/4.0.5 Mobile/8A293 Safari/6531.22.7", + UserAgent{ + Browser{BrowserSafari, Version{4, 0, 5}}, OS{PlatformiPhone, OSiOS, Version{4, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", + UserAgent{ + Browser{BrowserSafari, Version{5, 1, 0}}, OS{PlatformiPhone, OSiOS, Version{5, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", + UserAgent{ + Browser{BrowserSafari, Version{5, 1, 0}}, OS{PlatformiPad, OSiOS, Version{5, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25", + UserAgent{ + Browser{BrowserSafari, Version{6, 0, 0}}, OS{PlatformiPad, OSiOS, Version{6, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/546.10 (KHTML, like Gecko) Version/6.0 Mobile/7E18WD Safari/8536.25", + UserAgent{ + Browser{BrowserSafari, Version{6, 0, 0}}, OS{PlatformiPhone, OSiOS, Version{7, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", + UserAgent{ + Browser{BrowserSafari, Version{7, 0, 0}}, OS{PlatformiPad, OSiOS, Version{7, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPad; CPU OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53", + UserAgent{ + Browser{BrowserSafari, Version{7, 0, 0}}, OS{PlatformiPad, OSiOS, Version{7, 0, 2}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Mobile/14D27 [FBAN/FBIOS;FBAV/86.0.0.48.52;FBBV/53842252;FBDV/iPhone9,1;FBMD/iPhone;FBSN/iOS;FBSV/10.2.1;FBSS/2;FBCR/Verizon;FBID/phone;FBLC/en_US;FBOP/5;FBRV/0]", + UserAgent{ + Browser{BrowserSafari, Version{10, 2, 1}}, OS{PlatformiPhone, OSiOS, Version{10, 2, 1}}, DevicePhone}}, + + // TODO handle default browser based on iOS version + // {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/538.34.9 (KHTML, like Gecko) Mobile/12A4265u", + // UserAgent{ + // Browser{BrowserSafari, Version{8,0,0}}, OS{PlatformiPhone, OSiOS, Version{8,0,0}}, DevicePhone}}, + + // TODO extrapolate browser from iOS version + // {"Mozilla/5.0 (iPad; CPU OS 8_0 like Mac OS X) AppleWebKit/538.34.9 (KHTML, like Gecko) Mobile/12A4265u", + // UserAgent{ + // Browser{BrowserSafari, Version{8,0,0}}, OS{PlatformiPad, OSiOS, Version{8,0,0}}, DeviceTablet}}, + + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A405 Safari/600.1.4", + UserAgent{ + Browser{BrowserSafari, Version{8, 0, 0}}, OS{PlatformiPhone, OSiOS, Version{8, 0, 2}}, DevicePhone}}, + + {"Mozilla/5.0 (X11; U; Linux x86_64; en; rv:1.9.0.14) Gecko/20080528 Ubuntu/9.10 (karmic) Epiphany/2.22 Firefox/3.0", + UserAgent{ + Browser{BrowserFirefox, Version{3, 0, 0}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceComputer}}, + + // Can't parse browser due to limitation of user agent library + {"Mozilla/5.0 (X11; U; Linux x86_64; zh-TW; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.04 (hardy) Firefox/3.0.8 GTB5", + UserAgent{ + Browser{BrowserFirefox, Version{3, 0, 8}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; Konqueror/3.5; Linux; x86_64) KHTML/3.5.5 (like Gecko) (Debian)", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.5) Gecko/20091112 Iceweasel/3.5.5 (like Firefox/3.5.5; Debian-3.5.5-1)", + UserAgent{ + Browser{BrowserFirefox, Version{3, 5, 5}}, OS{PlatformLinux, OSLinux, Version{0, 0, 0}}, DeviceComputer}}, + + // TODO consider bot? + // {"Miro/2.0.4 (http://www.getmiro.com/; Darwin 10.3.0 i386)", + // UserAgent{ + // Browser{BrowserUnknown, Version{0,0,0}}, OS{PlatformMac, OSMacOSX, Version{3,0,0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; en-US; rv:1.9.1b3pre) Gecko/20090223 SeaMonkey/2.0a3", + UserAgent{ + Browser{BrowserFirefox, Version{0, 0, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 4, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.12", + UserAgent{ + Browser{BrowserSafari, Version{3, 2, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 5, 5}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en; rv:1.9.0.8pre) Gecko/2009022800 Camino/2.0b3pre", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 5, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.1 (KHTML, like Gecko) Chrome/5.0.329.0 Safari/533.1", + UserAgent{ + Browser{BrowserChrome, Version{5, 0, 329}}, OS{PlatformMac, OSMacOSX, Version{10, 6, 2}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 3.5.30729)", + UserAgent{ + Browser{BrowserFirefox, Version{3, 5, 6}}, OS{PlatformMac, OSMacOSX, Version{10, 6, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/534.52.7 (KHTML, like Gecko) Version/5.1.2 Safari/534.52.7", + UserAgent{ + Browser{BrowserSafari, Version{5, 1, 2}}, OS{PlatformMac, OSMacOSX, Version{10, 7, 2}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:9.0) Gecko/20111222 Thunderbird/9.0.1", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 7, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7", + UserAgent{ + Browser{BrowserChrome, Version{16, 0, 912}}, OS{PlatformMac, OSMacOSX, Version{10, 7, 2}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8) AppleWebKit/535.18.5 (KHTML, like Gecko) Version/5.2 Safari/535.18.5", + UserAgent{ + Browser{BrowserSafari, Version{5, 2, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 8, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_8; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5", + UserAgent{ + Browser{BrowserChrome, Version{4, 0, 249}}, OS{PlatformMac, OSMacOSX, Version{10, 8, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9) AppleWebKit/537.35.1 (KHTML, like Gecko) Version/6.1 Safari/537.35.1", + UserAgent{ + Browser{BrowserSafari, Version{6, 1, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 9, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/538.34.48 (KHTML, like Gecko) Version/8.0 Safari/538.35.8", + UserAgent{ + Browser{BrowserSafari, Version{8, 0, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 10, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/538.32 (KHTML, like Gecko) Version/7.1 Safari/538.4", + UserAgent{ + Browser{BrowserSafari, Version{7, 1, 0}}, OS{PlatformMac, OSMacOSX, Version{10, 10, 0}}, DeviceComputer}}, + + {"Opera/9.80 (S60; SymbOS; Opera Mobi/352; U; de) Presto/2.4.15 Version/10.00", + UserAgent{ + Browser{BrowserOpera, Version{10, 0, 0}}, OS{PlatformUnknown, OSUnknown, Version{0, 0, 0}}, DevicePhone}}, + + {"Opera/9.80 (S60; SymbOS; Opera Mobi/352; U; de) Presto/2.4.15 Version/10.00", + UserAgent{ + Browser{BrowserOpera, Version{10, 0, 0}}, OS{PlatformUnknown, OSUnknown, Version{0, 0, 0}}, DevicePhone}}, + + // TODO: support OneBrowser? https://play.google.com/store/apps/details?id=com.tencent.ibibo.mtt&hl=en_GB + // {"OneBrowser/3.1 (NokiaN70-1/5.0638.3.0.1)", + // UserAgent{ + // Browser{BrowserUnknown, Version{0,0,0}}, OS{PlatformUnknown, OSUnknown, Version{0,0,0}}, DevicePhone}}, + + // WebOS reports itself as safari :( + {"Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0", + UserAgent{ + Browser{BrowserUnknown, Version{1, 0, 0}}, OS{PlatformLinux, OSWebOS, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (webOS/1.4.1.1; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pre/1.0", + UserAgent{ + Browser{BrowserUnknown, Version{1, 0, 0}}, OS{PlatformLinux, OSWebOS, Version{0, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformLinux, OSWebOS, Version{0, 0, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformLinux, OSWebOS, Version{0, 0, 0}}, DeviceTablet}}, + + {"Opera/9.30 (Nintendo Wii; U; ; 2047-7; fr)", + UserAgent{ + Browser{BrowserOpera, Version{9, 30, 0}}, OS{PlatformNintendo, OSNintendo, Version{0, 0, 0}}, DeviceConsole}}, + + {"Mozilla/5.0 (Nintendo WiiU) AppleWebKit/534.52 (KHTML, like Gecko) NX/2.1.0.8.21 NintendoBrowser/1.0.0.7494.US", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformNintendo, OSNintendo, Version{0, 0, 0}}, DeviceConsole}}, + + {"Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.6 NintendoBrowser/2.0.0.9362.US", + UserAgent{ + Browser{BrowserUnknown, Version{0, 0, 0}}, OS{PlatformNintendo, OSNintendo, Version{0, 0, 0}}, DeviceConsole}}, + + // TODO fails to get opera first -- but is this a real UA string or an uncommon spoof? + // {"Mozilla/4.0 (compatible; MSIE 5.0; Windows 2000) Opera 6.0 [en]", + // BrowserIE, Version{5,0,0}}, OS{PlatformWindows, OSWindows, Version{4,0,0}}, DeviceComputer}}, + + {"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1; .NET CLR 1.1.4322; .NET CLR 1.0.3705; .NET CLR 2.0.50727)", + UserAgent{ + Browser{BrowserIE, Version{5, 0, 1}}, OS{PlatformWindows, OSWindows, Version{5, 0, 0}}, DeviceComputer}}, + + {"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/4.0; GTB6.4; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; OfficeLiveConnector.1.3; OfficeLivePatch.0.0; .NET CLR 1.1.4322)", + UserAgent{ + Browser{BrowserIE, Version{7, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 1, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Windows; U; Windows NT 6.1; sk; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7", + UserAgent{ + Browser{BrowserFirefox, Version{3, 5, 7}}, OS{PlatformWindows, OSWindows, Version{6, 1, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)", + UserAgent{ + Browser{BrowserIE, Version{10, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 2, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5402 Safari/536.5", + UserAgent{ + Browser{BrowserChrome, Version{19, 0, 1084}}, OS{PlatformWindows, OSWindows, Version{6, 2, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15", + UserAgent{ + Browser{BrowserChrome, Version{24, 0, 1295}}, OS{PlatformWindows, OSWindows, Version{6, 2, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko", + UserAgent{ + Browser{BrowserIE, Version{11, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 3, 0}}, DeviceTablet}}, + + {"Mozilla/5.0 (IE 11.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C; rv:11.0) like Gecko", + UserAgent{ + Browser{BrowserIE, Version{11, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 3, 0}}, DeviceComputer}}, + + // {"Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)", + // UserAgent{ + // Browser{BrowserIE, Version{5,0,0}}, OS{PlatformWindows, OSWindows95, Version{5,0,0}}, DeviceComputer}}, + + // {"Mozilla/4.0 (compatible; MSIE 5.0; Windows 95) Opera 6.02 [en]", + // UserAgent{ + // Browser{BrowserIE, Version{5,0,0}}, OS{PlatformWindows, OSWindows95, Version{5,0,0}}, DeviceComputer}}, + + // {"Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98; YComp 5.0.0.0)", + // UserAgent{ + // Browser{BrowserIE, Version{6,0,0}}, OS{PlatformWindows, OSWindows98, Version{5,0,0}}, DeviceComputer}}, + + // {"Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)", + // UserAgent{ + // Browser{BrowserIE, Version{4,0,0}}, OS{PlatformWindows, OSWindows98, Version{5,0,0}}, DeviceComputer}}, + + // {"Mozilla/5.0 (Windows; U; Windows 98; en-US; rv:1.8.1.8pre) Gecko/20071019 Firefox/2.0.0.8 Navigator/9.0.0.1", + // UserAgent{ + // Browser{BrowserFirefox, Version{2,0,0}}, OS{PlatformWindows, OSWindows98, Version{5,0,0}}, DeviceComputer}}, + + //Can't parse due to limitation of user agent library + // {"Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016", + // UserAgent{ + // Browser{ BrowserUnknown, Version{0,0,0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{0,0,0}}, DevicePhone}}, + + // {"Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; 176x220)", + // UserAgent{ + // Browser{BrowserIE, Version{4,0,0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{0,0,0}}, DevicePhone}}, + + // Can't parse browser due to limitation of user agent library + // {"Mozilla/4.0 (compatible; MSIE 5.0; Windows ME) Opera 6.0 [de]", + // UserAgent{ + // Browser{BrowserUnknown, OSWindowsME}, DeviceComputer}}, + + {"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 1.1.4322; InfoPath.2; .NET CLR 3.5.21022; .NET CLR 3.5.30729; MS-RTC LM 8; OfficeLiveConnector.1.4; OfficeLivePatch.1.3; .NET CLR 3.0.30729)", + UserAgent{ + Browser{BrowserIE, Version{8, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 0, 0}}, DeviceComputer}}, + + {"Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8", + UserAgent{ + Browser{BrowserFirefox, Version{3, 5, 8}}, OS{PlatformWindows, OSWindows, Version{5, 1, 0}}, DeviceComputer}}, + + {"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; )", + UserAgent{ + Browser{BrowserIE, Version{7, 0, 0}}, OS{PlatformWindows, OSWindows, Version{5, 1, 0}}, DeviceComputer}}, + + // Can't parse due to limitation of user agent library + {"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Windows Phone 6.5.3.5)", + UserAgent{ + Browser{BrowserIE, Version{6, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{6, 5, 3}}, DevicePhone}}, + + // desktop mode for Windows Phone 7 + {"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; XBLWP7; ZuneWP7)", + UserAgent{ + Browser{BrowserIE, Version{7, 0, 0}}, OS{PlatformWindows, OSWindows, Version{6, 1, 0}}, DeviceComputer}}, + + // mobile mode for Windows Phone 7 + {"Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0; HTC; T8788)", + UserAgent{ + Browser{BrowserIE, Version{7, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{7, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)", + UserAgent{ + Browser{BrowserIE, Version{9, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{7, 5, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)", + UserAgent{ + Browser{BrowserIE, Version{10, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{8, 0, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch IEMobile/11.0; HTC; Windows Phone 8S by HTC) like Gecko", + UserAgent{ + Browser{BrowserIE, Version{11, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{8, 1, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch IEMobile/11.0; NOKIA; 909) like Gecko", + UserAgent{ + Browser{BrowserIE, Version{11, 0, 0}}, OS{PlatformWindowsPhone, OSWindowsPhone, Version{8, 1, 0}}, DevicePhone}}, + + {"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; Xbox)", + UserAgent{ + Browser{BrowserIE, Version{9, 0, 0}}, OS{PlatformXbox, OSXbox, Version{6, 1, 0}}, DeviceConsole}}, +} + +func TestAgentSurfer(t *testing.T) { + for _, determined := range testUAVars { + t.Run("", func(t *testing.T) { + testFuncs := []func(string) *UserAgent{ + Parse, + func(ua string) *UserAgent { + u := new(UserAgent) + ParseUserAgent(ua, u) + return u + }, + } + + for _, f := range testFuncs { + ua := f(determined.UA) + + if ua.Browser.Name != determined.Browser.Name { + t.Errorf("browserName: got %v, wanted %v", ua.Browser.Name, determined.Browser.Name) + t.Logf("agent: %s", determined.UA) + } + + if ua.Browser.Version != determined.Browser.Version { + t.Errorf("browser version: got %d, wanted %d", ua.Browser.Version, determined.Browser.Version) + t.Logf("agent: %s", determined.UA) + } + + if ua.OS.Platform != determined.OS.Platform { + t.Errorf("platform: got %v, wanted %v", ua.OS.Platform, determined.OS.Platform) + t.Logf("agent: %s", determined.UA) + } + + if ua.OS.Name != determined.OS.Name { + t.Errorf("os: got %s, wanted %s", ua.OS.Name, determined.OS.Name) + t.Logf("agent: %s", determined.UA) + } + + if ua.OS.Version != determined.OS.Version { + t.Errorf("os version: got %d, wanted %d", ua.OS.Version, determined.OS.Version) + t.Logf("agent: %s", determined.UA) + } + + if ua.DeviceType != determined.DeviceType { + t.Errorf("device type: got %v, wanted %v", ua.DeviceType, determined.DeviceType) + t.Logf("agent: %s", determined.UA) + } + } + }) + } +} + +func BenchmarkAgentSurfer(b *testing.B) { + num := len(testUAVars) + b.ResetTimer() + for i := 0; i < b.N; i++ { + Parse(testUAVars[i%num].UA) + } +} + +func BenchmarkAgentSurferReuse(b *testing.B) { + dest := new(UserAgent) + num := len(testUAVars) + b.ResetTimer() + for i := 0; i < b.N; i++ { + dest.Reset() + ParseUserAgent(testUAVars[i%num].UA, dest) + } +} + +func BenchmarkEvalSystem(b *testing.B) { + num := len(testUAVars) + v := UserAgent{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.evalOS(testUAVars[i%num].UA) + } +} + +func BenchmarkEvalBrowserName(b *testing.B) { + num := len(testUAVars) + v := UserAgent{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.evalBrowserName(testUAVars[i%num].UA) + } +} + +func BenchmarkEvalBrowserVersion(b *testing.B) { + num := len(testUAVars) + v := UserAgent{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.Browser.Name = testUAVars[i%num].Browser.Name + v.evalBrowserVersion(testUAVars[i%num].UA) + } +} + +func BenchmarkEvalDevice(b *testing.B) { + num := len(testUAVars) + v := UserAgent{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.OS.Name = testUAVars[i%num].OS.Name + v.OS.Platform = testUAVars[i%num].OS.Platform + v.Browser.Name = testUAVars[i%num].Browser.Name + v.evalDevice(testUAVars[i%num].UA) + } +} + +// Chrome for Mac +func BenchmarkParseChromeMac(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + Parse("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36") + } +} + +// Chrome for Windows +func BenchmarkParseChromeWin(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + Parse("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36") + } +} + +// Chrome for Android +func BenchmarkParseChromeAndroid(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + Parse("Mozilla/5.0 (Linux; Android 4.4.2; GT-P5210 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Safari/537.36") + } +} + +// Safari for Mac +func BenchmarkParseSafariMac(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + Parse("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12") + } +} + +// Safari for iPad +func BenchmarkParseSafariiPad(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + Parse("Mozilla/5.0 (iPad; CPU OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B440 Safari/600.1.4") + } +} diff --git a/vendor/github.com/mssola/user_agent/.travis.yml b/vendor/github.com/mssola/user_agent/.travis.yml deleted file mode 100644 index 96f43d112..000000000 --- a/vendor/github.com/mssola/user_agent/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go -go: - - 1.4.x - - 1.5.x - - 1.6.x - - 1.7.x - - 1.8.x - - 1.9.x - - 1.x - - tip -matrix: - allow_failures: - - go: tip diff --git a/vendor/github.com/mssola/user_agent/LICENSE b/vendor/github.com/mssola/user_agent/LICENSE deleted file mode 100644 index 1c42691ce..000000000 --- a/vendor/github.com/mssola/user_agent/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2012-2018 Miquel Sabaté Solà - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/mssola/user_agent/README.md b/vendor/github.com/mssola/user_agent/README.md deleted file mode 100644 index 60427ec53..000000000 --- a/vendor/github.com/mssola/user_agent/README.md +++ /dev/null @@ -1,51 +0,0 @@ - -# UserAgent [![Build Status](https://travis-ci.org/mssola/user_agent.png?branch=master)](https://travis-ci.org/mssola/user_agent) [![GoDoc](https://godoc.org/github.com/mssola/user_agent?status.png)](http://godoc.org/github.com/mssola/user_agent) - - -UserAgent is a Go library that parses HTTP User Agents. - -## Usage - -~~~ go -package main - -import ( - "fmt" - - "github.com/mssola/user_agent" -) - -func main() { - // The "New" function will create a new UserAgent object and it will parse - // the given string. If you need to parse more strings, you can re-use - // this object and call: ua.Parse("another string") - ua := user_agent.New("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11") - - fmt.Printf("%v\n", ua.Mobile()) // => false - fmt.Printf("%v\n", ua.Bot()) // => false - fmt.Printf("%v\n", ua.Mozilla()) // => "5.0" - - fmt.Printf("%v\n", ua.Platform()) // => "X11" - fmt.Printf("%v\n", ua.OS()) // => "Linux x86_64" - - name, version := ua.Engine() - fmt.Printf("%v\n", name) // => "AppleWebKit" - fmt.Printf("%v\n", version) // => "537.11" - - name, version = ua.Browser() - fmt.Printf("%v\n", name) // => "Chrome" - fmt.Printf("%v\n", version) // => "23.0.1271.97" - - // Let's see an example with a bot. - - ua.Parse("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)") - - fmt.Printf("%v\n", ua.Bot()) // => true - - name, version = ua.Browser() - fmt.Printf("%v\n", name) // => Googlebot - fmt.Printf("%v\n", version) // => 2.1 -} -~~~ - -Copyright © 2012-2018 Miquel Sabaté Solà, released under the MIT License. diff --git a/vendor/github.com/mssola/user_agent/all_test.go b/vendor/github.com/mssola/user_agent/all_test.go deleted file mode 100644 index a5d5ee648..000000000 --- a/vendor/github.com/mssola/user_agent/all_test.go +++ /dev/null @@ -1,594 +0,0 @@ -// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com> -// This file is licensed under the MIT license. -// See the LICENSE file. - -package user_agent - -import ( - "fmt" - "reflect" - "testing" -) - -// Slice that contains all the tests. Each test is contained in a struct -// that groups the title of the test, the User-Agent string to be tested and the expected value. -var uastrings = []struct { - title string - ua string - expected string - expectedOS *OSInfo -}{ - // Bots - { - title: "GoogleBot", - ua: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", - expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:false", - }, - { - title: "GoogleBotSmartphone (iPhone)", - ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", - expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:true", - }, - { - title: "GoogleBotSmartphone (Android)", - ua: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", - expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:true", - }, - { - title: "BingBot", - ua: "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", - expected: "Mozilla:5.0 Browser:bingbot-2.0 Bot:true Mobile:false", - }, - { - title: "BaiduBot", - ua: "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)", - expected: "Mozilla:5.0 Browser:Baiduspider-2.0 Bot:true Mobile:false", - }, - { - title: "Twitterbot", - ua: "Twitterbot", - expected: "Browser:Twitterbot Bot:true Mobile:false", - }, - { - title: "YahooBot", - ua: "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)", - expected: "Mozilla:5.0 Browser:Yahoo! Slurp Bot:true Mobile:false", - }, - { - title: "FacebookExternalHit", - ua: "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)", - expected: "Browser:facebookexternalhit-1.1 Bot:true Mobile:false", - }, - { - title: "FacebookPlatform", - ua: "facebookplatform/1.0 (+http://developers.facebook.com)", - expected: "Browser:facebookplatform-1.0 Bot:true Mobile:false", - }, - { - title: "FaceBot", - ua: "Facebot", - expected: "Browser:Facebot Bot:true Mobile:false", - }, - { - title: "NutchCVS", - ua: "NutchCVS/0.8-dev (Nutch; http://lucene.apache.org/nutch/bot.html; nutch-agent@lucene.apache.org)", - expected: "Browser:NutchCVS Bot:true Mobile:false", - }, - { - title: "MJ12bot", - ua: "Mozilla/5.0 (compatible; MJ12bot/v1.2.4; http://www.majestic12.co.uk/bot.php?+)", - expected: "Mozilla:5.0 Browser:MJ12bot-v1.2.4 Bot:true Mobile:false", - }, - { - title: "MJ12bot", - ua: "MJ12bot/v1.0.8 (http://majestic12.co.uk/bot.php?+)", - expected: "Browser:MJ12bot Bot:true Mobile:false", - }, - { - title: "AhrefsBot", - ua: "Mozilla/5.0 (compatible; AhrefsBot/4.0; +http://ahrefs.com/robot/)", - expected: "Mozilla:5.0 Browser:AhrefsBot-4.0 Bot:true Mobile:false", - }, - - // Internet Explorer - { - title: "IE10", - ua: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows 8", "Windows", "8"}, - }, - { - title: "Tablet", - ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; ARM; Trident/6.0; Touch; .NET4.0E; .NET4.0C; Tablet PC 2.0)", - expected: "Mozilla:4.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", - }, - { - title: "Touch", - ua: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; ARM; Trident/6.0; Touch)", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", - }, - { - title: "Phone", - ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0; SAMSUNG; SGH-i917)", - expected: "Mozilla:4.0 Platform:Windows OS:Windows Phone OS 7.0 Browser:Internet Explorer-7.0 Engine:Trident Bot:false Mobile:true", - expectedOS: &OSInfo{"Windows Phone OS 7.0", "Windows Phone OS", "7.0"}, - }, - { - title: "IE6", - ua: "Mozilla/4.0 (compatible; MSIE6.0; Windows NT 5.0; .NET CLR 1.1.4322)", - expected: "Mozilla:4.0 Platform:Windows OS:Windows 2000 Browser:Internet Explorer-6.0 Engine:Trident Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows 2000", "Windows", "2000"}, - }, - { - title: "IE8Compatibility", - ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; MS-RTC LM 8)", - expected: "Mozilla:4.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-8.0 Engine:Trident Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows 7", "Windows", "7"}, - }, - { - title: "IE10Compatibility", - ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; MS-RTC LM 8)", - expected: "Mozilla:4.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", - }, - { - title: "IE11Win81", - ua: "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 8.1 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows 8.1", "Windows", "8.1"}, - }, - { - title: "IE11Win7", - ua: "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", - }, - { - title: "IE11b32Win7b64", - ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", - }, - { - title: "IE11b32Win7b64MDDRJS", - ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDRJS; rv:11.0) like Gecko", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", - }, - { - title: "IE11Compatibility", - ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0)", - expected: "Mozilla:4.0 Platform:Windows OS:Windows 8.1 Browser:Internet Explorer-7.0 Engine:Trident Bot:false Mobile:false", - }, - - // Microsoft Edge - { - title: "EdgeDesktop", - ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 10 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows 10", "Windows", "10"}, - }, - { - title: "EdgeMobile", - ua: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; DEVICE INFO) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Mobile Safari/537.36 Edge/12.10240", - expected: "Mozilla:5.0 Platform:Windows OS:Windows Phone 10.0 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:true", - }, - - // Gecko - { - title: "FirefoxMac", - ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8", - expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Browser:Firefox-4.0b8 Engine:Gecko-20100101 Bot:false Mobile:false", - expectedOS: &OSInfo{"Intel Mac OS X 10.6", "Mac OS X", "10.6"}, - }, - { - title: "FirefoxMacLoc", - ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13", - expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Localization:en-US Browser:Firefox-3.6.13 Engine:Gecko-20101203 Bot:false Mobile:false", - expectedOS: &OSInfo{"Intel Mac OS X 10.6", "Mac OS X", "10.6"}, - }, - { - title: "FirefoxLinux", - ua: "Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0", - expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Firefox-17.0 Engine:Gecko-20100101 Bot:false Mobile:false", - expectedOS: &OSInfo{"Linux x86_64", "Linux", ""}, - }, - { - title: "FirefoxLinux - Ubuntu V50", - ua: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0", - expected: "Mozilla:5.0 Platform:X11 OS:Ubuntu Browser:Firefox-50.0 Engine:Gecko-20100101 Bot:false Mobile:false", - expectedOS: &OSInfo{"Ubuntu", "Ubuntu", ""}, - }, - { - title: "FirefoxWin", - ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.14", - expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en-US Browser:Firefox-2.0.0.14 Engine:Gecko-20080404 Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows XP", "Windows", "XP"}, - }, - { - title: "Firefox29Win7", - ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Firefox-29.0 Engine:Gecko-20100101 Bot:false Mobile:false", - }, - { - title: "CaminoMac", - ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.14) Gecko/20080409 Camino/1.6 (like Firefox/2.0.0.14)", - expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X Localization:en Browser:Camino-1.6 Engine:Gecko-20080409 Bot:false Mobile:false", - expectedOS: &OSInfo{"Intel Mac OS X", "Mac OS X", ""}, - }, - { - title: "Iceweasel", - ua: "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061024 Iceweasel/2.0 (Debian-2.0+dfsg-1)", - expected: "Mozilla:5.0 Platform:X11 OS:Linux i686 Localization:en-US Browser:Iceweasel-2.0 Engine:Gecko-20061024 Bot:false Mobile:false", - expectedOS: &OSInfo{"Linux i686", "Linux", ""}, - }, - { - title: "SeaMonkey", - ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.4) Gecko/20091017 SeaMonkey/2.0", - expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Localization:en-US Browser:SeaMonkey-2.0 Engine:Gecko-20091017 Bot:false Mobile:false", - }, - { - title: "AndroidFirefox", - ua: "Mozilla/5.0 (Android; Mobile; rv:17.0) Gecko/17.0 Firefox/17.0", - expected: "Mozilla:5.0 Platform:Mobile OS:Android Browser:Firefox-17.0 Engine:Gecko-17.0 Bot:false Mobile:true", - }, - { - title: "AndroidFirefoxTablet", - ua: "Mozilla/5.0 (Android; Tablet; rv:26.0) Gecko/26.0 Firefox/26.0", - expected: "Mozilla:5.0 Platform:Tablet OS:Android Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true", - expectedOS: &OSInfo{"Android", "Android", ""}, - }, - { - title: "FirefoxOS", - ua: "Mozilla/5.0 (Mobile; rv:26.0) Gecko/26.0 Firefox/26.0", - expected: "Mozilla:5.0 Platform:Mobile OS:FirefoxOS Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true", - expectedOS: &OSInfo{"FirefoxOS", "FirefoxOS", ""}, - }, - { - title: "FirefoxOSTablet", - ua: "Mozilla/5.0 (Tablet; rv:26.0) Gecko/26.0 Firefox/26.0", - expected: "Mozilla:5.0 Platform:Tablet OS:FirefoxOS Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true", - }, - { - title: "FirefoxWinXP", - ua: "Mozilla/5.0 (Windows NT 5.2; rv:31.0) Gecko/20100101 Firefox/31.0", - expected: "Mozilla:5.0 Platform:Windows OS:Windows XP x64 Edition Browser:Firefox-31.0 Engine:Gecko-20100101 Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows XP x64 Edition", "Windows", "XP"}, - }, - { - title: "FirefoxMRA", - ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:24.0) Gecko/20130405 MRA 5.5 (build 02842) Firefox/24.0 (.NET CLR 3.5.30729)", - expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en-US Browser:Firefox-24.0 Engine:Gecko-20130405 Bot:false Mobile:false", - }, - - // Opera - { - title: "OperaMac", - ua: "Opera/9.27 (Macintosh; Intel Mac OS X; U; en)", - expected: "Platform:Macintosh OS:Intel Mac OS X Localization:en Browser:Opera-9.27 Engine:Presto Bot:false Mobile:false", - expectedOS: &OSInfo{"Intel Mac OS X", "Mac OS X", ""}, - }, - { - title: "OperaWin", - ua: "Opera/9.27 (Windows NT 5.1; U; en)", - expected: "Platform:Windows OS:Windows XP Localization:en Browser:Opera-9.27 Engine:Presto Bot:false Mobile:false", - }, - { - title: "OperaWinNoLocale", - ua: "Opera/9.80 (Windows NT 5.1) Presto/2.12.388 Version/12.10", - expected: "Platform:Windows OS:Windows XP Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", - }, - { - title: "OperaWin2Comment", - ua: "Opera/9.80 (Windows NT 6.0; WOW64) Presto/2.12.388 Version/12.15", - expected: "Platform:Windows OS:Windows Vista Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", - expectedOS: &OSInfo{"Windows Vista", "Windows", "Vista"}, - }, - { - title: "OperaMinimal", - ua: "Opera/9.80", - expected: "Browser:Opera-9.80 Engine:Presto Bot:false Mobile:false", - }, - { - title: "OperaFull", - ua: "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10", - expected: "Platform:Windows OS:Windows Vista Localization:en Browser:Opera-9.80 Engine:Presto-2.2.15 Bot:false Mobile:false", - }, - { - title: "OperaLinux", - ua: "Opera/9.80 (X11; Linux x86_64) Presto/2.12.388 Version/12.10", - expected: "Platform:X11 OS:Linux x86_64 Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", - }, - { - title: "OperaLinux - Ubuntu V41", - ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36 OPR/41.0.2353.69", - expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Opera-41.0.2353.69 Engine:AppleWebKit-537.36 Bot:false Mobile:false", - expectedOS: &OSInfo{"Linux x86_64", "Linux", ""}, - }, - { - title: "OperaAndroid", - ua: "Opera/9.80 (Android 4.2.1; Linux; Opera Mobi/ADR-1212030829) Presto/2.11.355 Version/12.10", - expected: "Platform:Android 4.2.1 OS:Linux Browser:Opera-9.80 Engine:Presto-2.11.355 Bot:false Mobile:true", - expectedOS: &OSInfo{"Linux", "Linux", ""}, - }, - { - title: "OperaNested", - ua: "Opera/9.80 (Windows NT 5.1; MRA 6.0 (build 5831)) Presto/2.12.388 Version/12.10", - expected: "Platform:Windows OS:Windows XP Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", - }, - { - title: "OperaMRA", - ua: "Opera/9.80 (Windows NT 6.1; U; MRA 5.8 (build 4139); en) Presto/2.9.168 Version/11.50", - expected: "Platform:Windows OS:Windows 7 Localization:en Browser:Opera-9.80 Engine:Presto-2.9.168 Bot:false Mobile:false", - }, - - // Other - { - title: "Empty", - ua: "", - expected: "Bot:false Mobile:false", - }, - { - title: "Nil", - ua: "nil", - expected: "Browser:nil Bot:false Mobile:false", - }, - { - title: "Compatible", - ua: "Mozilla/4.0 (compatible)", - expected: "Browser:Mozilla-4.0 Bot:false Mobile:false", - }, - { - title: "Mozilla", - ua: "Mozilla/5.0", - expected: "Browser:Mozilla-5.0 Bot:false Mobile:false", - }, - { - title: "Amaya", - ua: "amaya/9.51 libwww/5.4.0", - expected: "Browser:amaya-9.51 Engine:libwww-5.4.0 Bot:false Mobile:false", - }, - { - title: "Rails", - ua: "Rails Testing", - expected: "Browser:Rails Engine:Testing Bot:false Mobile:false", - }, - { - title: "Python", - ua: "Python-urllib/2.7", - expected: "Browser:Python-urllib-2.7 Bot:false Mobile:false", - }, - { - title: "Curl", - ua: "curl/7.28.1", - expected: "Browser:curl-7.28.1 Bot:false Mobile:false", - }, - - // WebKit - { - title: "ChromeLinux", - ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11", - expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chrome-23.0.1271.97 Engine:AppleWebKit-537.11 Bot:false Mobile:false", - expectedOS: &OSInfo{"Linux x86_64", "Linux", ""}, - }, - { - title: "ChromeLinux - Ubuntu V55", - ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36", - expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chrome-55.0.2883.75 Engine:AppleWebKit-537.36 Bot:false Mobile:false", - }, - { - title: "ChromeWin7", - ua: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.168 Safari/535.19", - expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Chrome-18.0.1025.168 Engine:AppleWebKit-535.19 Bot:false Mobile:false", - }, - { - title: "ChromeMinimal", - ua: "Mozilla/5.0 AppleWebKit/534.10 Chrome/8.0.552.215 Safari/534.10", - expected: "Mozilla:5.0 Browser:Chrome-8.0.552.215 Engine:AppleWebKit-534.10 Bot:false Mobile:false", - }, - { - title: "ChromeMac", - ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.231 Safari/534.10", - expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10_6_5 Localization:en-US Browser:Chrome-8.0.552.231 Engine:AppleWebKit-534.10 Bot:false Mobile:false", - expectedOS: &OSInfo{"Intel Mac OS X 10_6_5", "Mac OS X", "10.6.5"}, - }, - { - title: "SafariMac", - ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16", - expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10_6_3 Localization:en-us Browser:Safari-5.0 Engine:AppleWebKit-533.16 Bot:false Mobile:false", - }, - { - title: "SafariWin", - ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8", - expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en Browser:Safari-4.0dp1 Engine:AppleWebKit-526.9 Bot:false Mobile:false", - }, - { - title: "iPhone7", - ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53", - expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 7_0_3 like Mac OS X Browser:Safari-7.0 Engine:AppleWebKit-537.51.1 Bot:false Mobile:true", - expectedOS: &OSInfo{"CPU iPhone OS 7_0_3 like Mac OS X", "iPhone OS", "7.0.3"}, - }, - { - title: "iPhone", - ua: "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/419", - expected: "Mozilla:5.0 Platform:iPhone OS:CPU like Mac OS X Localization:en Browser:Safari-3.0 Engine:AppleWebKit-420.1 Bot:false Mobile:true", - }, - { - title: "iPod", - ua: "Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/419", - expected: "Mozilla:5.0 Platform:iPod OS:CPU like Mac OS X Localization:en Browser:Safari-3.0 Engine:AppleWebKit-420.1 Bot:false Mobile:true", - }, - { - title: "iPad", - ua: "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10", - expected: "Mozilla:5.0 Platform:iPad OS:CPU OS 3_2 like Mac OS X Localization:en-us Browser:Safari-4.0.4 Engine:AppleWebKit-531.21.10 Bot:false Mobile:true", - }, - { - title: "webOS", - ua: "Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pre/1.1", - expected: "Mozilla:5.0 Platform:webOS OS:Palm Localization:en-US Browser:webOS-1.0 Engine:AppleWebKit-532.2 Bot:false Mobile:true", - }, - { - title: "Android", - ua: "Mozilla/5.0 (Linux; U; Android 1.5; de-; HTC Magic Build/PLAT-RC33) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1", - expected: "Mozilla:5.0 Platform:Linux OS:Android 1.5 Localization:de- Browser:Android-3.1.2 Engine:AppleWebKit-528.5+ Bot:false Mobile:true", - }, - { - title: "BlackBerry", - ua: "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, Like Gecko) Version/6.0.0.141 Mobile Safari/534.1+", - expected: "Mozilla:5.0 Platform:BlackBerry OS:BlackBerry 9800 Localization:en Browser:BlackBerry-6.0.0.141 Engine:AppleWebKit-534.1+ Bot:false Mobile:true", - expectedOS: &OSInfo{"BlackBerry 9800", "BlackBerry", "9800"}, - }, - { - title: "BB10", - ua: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.3+ (KHTML, like Gecko) Version/10.0.9.388 Mobile Safari/537.3+", - expected: "Mozilla:5.0 Platform:BlackBerry OS:BlackBerry Browser:BlackBerry-10.0.9.388 Engine:AppleWebKit-537.3+ Bot:false Mobile:true", - }, - { - title: "Ericsson", - ua: "Mozilla/5.0 (SymbianOS/9.4; U; Series60/5.0 Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 Safari/525", - expected: "Mozilla:5.0 Platform:Symbian OS:SymbianOS/9.4 Browser:Symbian-3.0 Engine:AppleWebKit-525 Bot:false Mobile:true", - expectedOS: &OSInfo{"SymbianOS/9.4", "SymbianOS", "9.4"}, - }, - { - title: "ChromeAndroid", - ua: "Mozilla/5.0 (Linux; Android 4.2.1; Galaxy Nexus Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", - expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.1 Browser:Chrome-18.0.1025.166 Engine:AppleWebKit-535.19 Bot:false Mobile:true", - }, - { - title: "WebkitNoPlatform", - ua: "Mozilla/5.0 (en-us) AppleWebKit/525.13 (KHTML, like Gecko; Google Web Preview) Version/3.1 Safari/525.13", - expected: "Mozilla:5.0 Platform:en-us Localization:en-us Browser:Safari-3.1 Engine:AppleWebKit-525.13 Bot:false Mobile:false", - }, - { - title: "OperaWebkitMobile", - ua: "Mozilla/5.0 (Linux; Android 4.2.2; Galaxy Nexus Build/JDQ39) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.58 Mobile Safari/537.31 OPR/14.0.1074.57453", - expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.2 Browser:Opera-14.0.1074.57453 Engine:AppleWebKit-537.31 Bot:false Mobile:true", - }, - { - title: "OperaWebkitDesktop", - ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.58 Safari/537.31 OPR/14.0.1074.57453", - expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Opera-14.0.1074.57453 Engine:AppleWebKit-537.31 Bot:false Mobile:false", - }, - { - title: "ChromeNothingAfterU", - ua: "Mozilla/5.0 (Linux; U) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4", - expected: "Mozilla:5.0 Platform:Linux OS:Linux Browser:Chrome-22.0.1229.79 Engine:AppleWebKit-537.4 Bot:false Mobile:false", - }, - { - title: "SafariOnSymbian", - ua: "Mozilla/5.0 (SymbianOS/9.1; U; [en-us]) AppleWebKit/413 (KHTML, like Gecko) Safari/413", - expected: "Mozilla:5.0 Platform:Symbian OS:SymbianOS/9.1 Browser:Symbian-413 Engine:AppleWebKit-413 Bot:false Mobile:true", - }, - { - title: "Chromium - Ubuntu V49", - ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36", - expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chromium-49.0.2623.108 Engine:AppleWebKit-537.36 Bot:false Mobile:false", - }, - { - title: "Chromium - Ubuntu V55", - ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/53.0.2785.143 Chrome/53.0.2785.143 Safari/537.36", - expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chromium-53.0.2785.143 Engine:AppleWebKit-537.36 Bot:false Mobile:false", - }, - - // Dalvik - { - title: "Dalvik - Dell:001DL", - ua: "Dalvik/1.2.0 (Linux; U; Android 2.2.2; 001DL Build/FRG83G)", - expected: "Mozilla:5.0 Platform:Linux OS:Android 2.2.2 Bot:false Mobile:true", - }, - { - title: "Dalvik - HTC:001HT", - ua: "Dalvik/1.4.0 (Linux; U; Android 2.3.3; 001HT Build/GRI40)", - expected: "Mozilla:5.0 Platform:Linux OS:Android 2.3.3 Bot:false Mobile:true", - }, - { - title: "Dalvik - ZTE:009Z", - ua: "Dalvik/1.4.0 (Linux; U; Android 2.3.4; 009Z Build/GINGERBREAD)", - expected: "Mozilla:5.0 Platform:Linux OS:Android 2.3.4 Bot:false Mobile:true", - }, - { - title: "Dalvik - A850", - ua: "Dalvik/1.6.0 (Linux; U; Android 4.2.2; A850 Build/JDQ39) Configuration/CLDC-1.1; Opera Mini/att/4.2", - expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.2 Bot:false Mobile:true", - }, - { - title: "Dalvik - Asus:T00Q", - ua: "Dalvik/1.6.0 (Linux; U; Android 4.4.2; ASUS_T00Q Build/KVT49L)/CLDC-1.1", - expected: "Mozilla:5.0 Platform:Linux OS:Android 4.4.2 Bot:false Mobile:true", - expectedOS: &OSInfo{"Android 4.4.2", "Android", "4.4.2"}, - }, - { - title: "Dalvik - W2430", - ua: "Dalvik/1.6.0 (Linux; U; Android 4.0.4; W2430 Build/IMM76D)014; Profile/MIDP-2.1 Configuration/CLDC-1", - expected: "Mozilla:5.0 Platform:Linux OS:Android 4.0.4 Bot:false Mobile:true", - }, -} - -// Internal: beautify the UserAgent reference into a string so it can be -// tested later on. -// -// ua - a UserAgent reference. -// -// Returns a string that contains the beautified representation. -func beautify(ua *UserAgent) (s string) { - if len(ua.Mozilla()) > 0 { - s += "Mozilla:" + ua.Mozilla() + " " - } - if len(ua.Platform()) > 0 { - s += "Platform:" + ua.Platform() + " " - } - if len(ua.OS()) > 0 { - s += "OS:" + ua.OS() + " " - } - if len(ua.Localization()) > 0 { - s += "Localization:" + ua.Localization() + " " - } - str1, str2 := ua.Browser() - if len(str1) > 0 { - s += "Browser:" + str1 - if len(str2) > 0 { - s += "-" + str2 + " " - } else { - s += " " - } - } - str1, str2 = ua.Engine() - if len(str1) > 0 { - s += "Engine:" + str1 - if len(str2) > 0 { - s += "-" + str2 + " " - } else { - s += " " - } - } - s += "Bot:" + fmt.Sprintf("%v", ua.Bot()) + " " - s += "Mobile:" + fmt.Sprintf("%v", ua.Mobile()) - return s -} - -// The test suite. -func TestUserAgent(t *testing.T) { - for _, tt := range uastrings { - ua := New(tt.ua) - got := beautify(ua) - if tt.expected != got { - t.Errorf("\nTest %v\ngot: %q\nexpected %q\n", tt.title, got, tt.expected) - } - - if tt.expectedOS != nil { - gotOSInfo := ua.OSInfo() - if !reflect.DeepEqual(tt.expectedOS, &gotOSInfo) { - t.Errorf("\nTest %v\ngot: %#v\nexpected %#v\n", tt.title, gotOSInfo, tt.expectedOS) - } - } - } -} - -// Benchmark: it parses each User-Agent string on the uastrings slice b.N times. -func BenchmarkUserAgent(b *testing.B) { - for i := 0; i < b.N; i++ { - b.StopTimer() - for _, tt := range uastrings { - ua := new(UserAgent) - b.StartTimer() - ua.Parse(tt.ua) - } - } -} diff --git a/vendor/github.com/mssola/user_agent/bot.go b/vendor/github.com/mssola/user_agent/bot.go deleted file mode 100644 index acccd51d6..000000000 --- a/vendor/github.com/mssola/user_agent/bot.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) 2014-2018 Miquel Sabaté Solà <mikisabate@gmail.com> -// This file is licensed under the MIT license. -// See the LICENSE file. - -package user_agent - -import ( - "regexp" - "strings" -) - -var botFromSiteRegexp = regexp.MustCompile("http://.+\\.\\w+") - -// Get the name of the bot from the website that may be in the given comment. If -// there is no website in the comment, then an empty string is returned. -func getFromSite(comment []string) string { - if len(comment) == 0 { - return "" - } - - // Where we should check the website. - idx := 2 - if len(comment) < 3 { - idx = 0 - } - - // Pick the site. - results := botFromSiteRegexp.FindStringSubmatch(comment[idx]) - if len(results) == 1 { - // If it's a simple comment, just return the name of the site. - if idx == 0 { - return results[0] - } - - // This is a large comment, usually the name will be in the previous - // field of the comment. - return strings.TrimSpace(comment[1]) - } - return "" -} - -// Returns true if the info that we currently have corresponds to the Google -// mobile bot. This function also modifies some attributes in the receiver -// accordingly. -func (p *UserAgent) googleBot() bool { - // This is a hackish way to detect Google's mobile bot. - if strings.Index(p.ua, "Googlebot") != -1 { - p.platform = "" - p.undecided = true - } - return p.undecided -} - -// Set the attributes of the receiver as given by the parameters. All the other -// parameters are set to empty. -func (p *UserAgent) setSimple(name, version string, bot bool) { - p.bot = bot - if !bot { - p.mozilla = "" - } - p.browser.Name = name - p.browser.Version = version - p.browser.Engine = "" - p.browser.EngineVersion = "" - p.os = "" - p.localization = "" -} - -// Fix some values for some weird browsers. -func (p *UserAgent) fixOther(sections []section) { - if len(sections) > 0 { - p.browser.Name = sections[0].name - p.browser.Version = sections[0].version - p.mozilla = "" - } -} - -var botRegex = regexp.MustCompile("(?i)(bot|crawler|sp(i|y)der|search|worm|fetch|nutch)") - -// Check if we're dealing with a bot or with some weird browser. If that is the -// case, the receiver will be modified accordingly. -func (p *UserAgent) checkBot(sections []section) { - // If there's only one element, and it's doesn't have the Mozilla string, - // check whether this is a bot or not. - if len(sections) == 1 && sections[0].name != "Mozilla" { - p.mozilla = "" - - // Check whether the name has some suspicious "bot" or "crawler" in his name. - if botRegex.Match([]byte(sections[0].name)) { - p.setSimple(sections[0].name, "", true) - return - } - - // Tough luck, let's try to see if it has a website in his comment. - if name := getFromSite(sections[0].comment); name != "" { - // First of all, this is a bot. Moreover, since it doesn't have the - // Mozilla string, we can assume that the name and the version are - // the ones from the first section. - p.setSimple(sections[0].name, sections[0].version, true) - return - } - - // At this point we are sure that this is not a bot, but some weirdo. - p.setSimple(sections[0].name, sections[0].version, false) - } else { - // Let's iterate over the available comments and check for a website. - for _, v := range sections { - if name := getFromSite(v.comment); name != "" { - // Ok, we've got a bot name. - results := strings.SplitN(name, "/", 2) - version := "" - if len(results) == 2 { - version = results[1] - } - p.setSimple(results[0], version, true) - return - } - } - - // We will assume that this is some other weird browser. - p.fixOther(sections) - } -} diff --git a/vendor/github.com/mssola/user_agent/browser.go b/vendor/github.com/mssola/user_agent/browser.go deleted file mode 100644 index c6a1d2469..000000000 --- a/vendor/github.com/mssola/user_agent/browser.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com> -// This file is licensed under the MIT license. -// See the LICENSE file. - -package user_agent - -import ( - "regexp" - "strings" -) - -var ie11Regexp = regexp.MustCompile("^rv:(.+)$") - -// A struct containing all the information that we might be -// interested from the browser. -type Browser struct { - // The name of the browser's engine. - Engine string - - // The version of the browser's engine. - EngineVersion string - - // The name of the browser. - Name string - - // The version of the browser. - Version string -} - -// Extract all the information that we can get from the User-Agent string -// about the browser and update the receiver with this information. -// -// The function receives just one argument "sections", that contains the -// sections from the User-Agent string after being parsed. -func (p *UserAgent) detectBrowser(sections []section) { - slen := len(sections) - - if sections[0].name == "Opera" { - p.browser.Name = "Opera" - p.browser.Version = sections[0].version - p.browser.Engine = "Presto" - if slen > 1 { - p.browser.EngineVersion = sections[1].version - } - } else if sections[0].name == "Dalvik" { - // When Dalvik VM is in use, there is no browser info attached to ua. - // Although browser is still a Mozilla/5.0 compatible. - p.mozilla = "5.0" - } else if slen > 1 { - engine := sections[1] - p.browser.Engine = engine.name - p.browser.EngineVersion = engine.version - if slen > 2 { - sectionIndex := 2 - // The version after the engine comment is empty on e.g. Ubuntu - // platforms so if this is the case, let's use the next in line. - if sections[2].version == "" && slen > 3 { - sectionIndex = 3 - } - p.browser.Version = sections[sectionIndex].version - if engine.name == "AppleWebKit" { - switch sections[slen-1].name { - case "Edge": - p.browser.Name = "Edge" - p.browser.Version = sections[slen-1].version - p.browser.Engine = "EdgeHTML" - p.browser.EngineVersion = "" - case "OPR": - p.browser.Name = "Opera" - p.browser.Version = sections[slen-1].version - default: - if sections[sectionIndex].name == "Chrome" { - p.browser.Name = "Chrome" - } else if sections[sectionIndex].name == "Chromium" { - p.browser.Name = "Chromium" - } else { - p.browser.Name = "Safari" - } - } - } else if engine.name == "Gecko" { - name := sections[2].name - if name == "MRA" && slen > 4 { - name = sections[4].name - p.browser.Version = sections[4].version - } - p.browser.Name = name - } else if engine.name == "like" && sections[2].name == "Gecko" { - // This is the new user agent from Internet Explorer 11. - p.browser.Engine = "Trident" - p.browser.Name = "Internet Explorer" - for _, c := range sections[0].comment { - version := ie11Regexp.FindStringSubmatch(c) - if len(version) > 0 { - p.browser.Version = version[1] - return - } - } - p.browser.Version = "" - } - } - } else if slen == 1 && len(sections[0].comment) > 1 { - comment := sections[0].comment - if comment[0] == "compatible" && strings.HasPrefix(comment[1], "MSIE") { - p.browser.Engine = "Trident" - p.browser.Name = "Internet Explorer" - // The MSIE version may be reported as the compatibility version. - // For IE 8 through 10, the Trident token is more accurate. - // http://msdn.microsoft.com/en-us/library/ie/ms537503(v=vs.85).aspx#VerToken - for _, v := range comment { - if strings.HasPrefix(v, "Trident/") { - switch v[8:] { - case "4.0": - p.browser.Version = "8.0" - case "5.0": - p.browser.Version = "9.0" - case "6.0": - p.browser.Version = "10.0" - } - break - } - } - // If the Trident token is not provided, fall back to MSIE token. - if p.browser.Version == "" { - p.browser.Version = strings.TrimSpace(comment[1][4:]) - } - } - } -} - -// Returns two strings. The first string is the name of the engine and the -// second one is the version of the engine. -func (p *UserAgent) Engine() (string, string) { - return p.browser.Engine, p.browser.EngineVersion -} - -// Returns two strings. The first string is the name of the browser and the -// second one is the version of the browser. -func (p *UserAgent) Browser() (string, string) { - return p.browser.Name, p.browser.Version -} diff --git a/vendor/github.com/mssola/user_agent/operating_systems.go b/vendor/github.com/mssola/user_agent/operating_systems.go deleted file mode 100644 index ad4d56680..000000000 --- a/vendor/github.com/mssola/user_agent/operating_systems.go +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com> -// This file is licensed under the MIT license. -// See the LICENSE file. - -package user_agent - -import "strings" - -// Represents full information on the operating system extracted from the user agent. -type OSInfo struct { - // Full name of the operating system. This is identical to the output of ua.OS() - FullName string - - // Name of the operating system. This is sometimes a shorter version of the - // operating system name, e.g. "Mac OS X" instead of "Intel Mac OS X" - Name string - - // Operating system version, e.g. 7 for Windows 7 or 10.8 for Max OS X Mountain Lion - Version string -} - -// Normalize the name of the operating system. By now, this just -// affects to Windows NT. -// -// Returns a string containing the normalized name for the Operating System. -func normalizeOS(name string) string { - sp := strings.SplitN(name, " ", 3) - if len(sp) != 3 || sp[1] != "NT" { - return name - } - - switch sp[2] { - case "5.0": - return "Windows 2000" - case "5.01": - return "Windows 2000, Service Pack 1 (SP1)" - case "5.1": - return "Windows XP" - case "5.2": - return "Windows XP x64 Edition" - case "6.0": - return "Windows Vista" - case "6.1": - return "Windows 7" - case "6.2": - return "Windows 8" - case "6.3": - return "Windows 8.1" - case "10.0": - return "Windows 10" - } - return name -} - -// Guess the OS, the localization and if this is a mobile device for a -// Webkit-powered browser. -// -// The first argument p is a reference to the current UserAgent and the second -// argument is a slice of strings containing the comment. -func webkit(p *UserAgent, comment []string) { - if p.platform == "webOS" { - p.browser.Name = p.platform - p.os = "Palm" - if len(comment) > 2 { - p.localization = comment[2] - } - p.mobile = true - } else if p.platform == "Symbian" { - p.mobile = true - p.browser.Name = p.platform - p.os = comment[0] - } else if p.platform == "Linux" { - p.mobile = true - if p.browser.Name == "Safari" { - p.browser.Name = "Android" - } - if len(comment) > 1 { - if comment[1] == "U" { - if len(comment) > 2 { - p.os = comment[2] - } else { - p.mobile = false - p.os = comment[0] - } - } else { - p.os = comment[1] - } - } - if len(comment) > 3 { - p.localization = comment[3] - } else if len(comment) == 3 { - _ = p.googleBot() - } - } else if len(comment) > 0 { - if len(comment) > 3 { - p.localization = comment[3] - } - if strings.HasPrefix(comment[0], "Windows NT") { - p.os = normalizeOS(comment[0]) - } else if len(comment) < 2 { - p.localization = comment[0] - } else if len(comment) < 3 { - if !p.googleBot() { - p.os = normalizeOS(comment[1]) - } - } else { - p.os = normalizeOS(comment[2]) - } - if p.platform == "BlackBerry" { - p.browser.Name = p.platform - if p.os == "Touch" { - p.os = p.platform - } - } - } -} - -// Guess the OS, the localization and if this is a mobile device -// for a Gecko-powered browser. -// -// The first argument p is a reference to the current UserAgent and the second -// argument is a slice of strings containing the comment. -func gecko(p *UserAgent, comment []string) { - if len(comment) > 1 { - if comment[1] == "U" { - if len(comment) > 2 { - p.os = normalizeOS(comment[2]) - } else { - p.os = normalizeOS(comment[1]) - } - } else { - if p.platform == "Android" { - p.mobile = true - p.platform, p.os = normalizeOS(comment[1]), p.platform - } else if comment[0] == "Mobile" || comment[0] == "Tablet" { - p.mobile = true - p.os = "FirefoxOS" - } else { - if p.os == "" { - p.os = normalizeOS(comment[1]) - } - } - } - // Only parse 4th comment as localization if it doesn't start with rv:. - // For example Firefox on Ubuntu contains "rv:XX.X" in this field. - if len(comment) > 3 && !strings.HasPrefix(comment[3], "rv:") { - p.localization = comment[3] - } - } -} - -// Guess the OS, the localization and if this is a mobile device -// for Internet Explorer. -// -// The first argument p is a reference to the current UserAgent and the second -// argument is a slice of strings containing the comment. -func trident(p *UserAgent, comment []string) { - // Internet Explorer only runs on Windows. - p.platform = "Windows" - - // The OS can be set before to handle a new case in IE11. - if p.os == "" { - if len(comment) > 2 { - p.os = normalizeOS(comment[2]) - } else { - p.os = "Windows NT 4.0" - } - } - - // Last but not least, let's detect if it comes from a mobile device. - for _, v := range comment { - if strings.HasPrefix(v, "IEMobile") { - p.mobile = true - return - } - } -} - -// Guess the OS, the localization and if this is a mobile device -// for Opera. -// -// The first argument p is a reference to the current UserAgent and the second -// argument is a slice of strings containing the comment. -func opera(p *UserAgent, comment []string) { - slen := len(comment) - - if strings.HasPrefix(comment[0], "Windows") { - p.platform = "Windows" - p.os = normalizeOS(comment[0]) - if slen > 2 { - if slen > 3 && strings.HasPrefix(comment[2], "MRA") { - p.localization = comment[3] - } else { - p.localization = comment[2] - } - } - } else { - if strings.HasPrefix(comment[0], "Android") { - p.mobile = true - } - p.platform = comment[0] - if slen > 1 { - p.os = comment[1] - if slen > 3 { - p.localization = comment[3] - } - } else { - p.os = comment[0] - } - } -} - -// Guess the OS. Android browsers send Dalvik as the user agent in the -// request header. -// -// The first argument p is a reference to the current UserAgent and the second -// argument is a slice of strings containing the comment. -func dalvik(p *UserAgent, comment []string) { - slen := len(comment) - - if strings.HasPrefix(comment[0], "Linux") { - p.platform = comment[0] - if slen > 2 { - p.os = comment[2] - } - p.mobile = true - } -} - -// Given the comment of the first section of the UserAgent string, -// get the platform. -func getPlatform(comment []string) string { - if len(comment) > 0 { - if comment[0] != "compatible" { - if strings.HasPrefix(comment[0], "Windows") { - return "Windows" - } else if strings.HasPrefix(comment[0], "Symbian") { - return "Symbian" - } else if strings.HasPrefix(comment[0], "webOS") { - return "webOS" - } else if comment[0] == "BB10" { - return "BlackBerry" - } - return comment[0] - } - } - return "" -} - -// Detect some properties of the OS from the given section. -func (p *UserAgent) detectOS(s section) { - if s.name == "Mozilla" { - // Get the platform here. Be aware that IE11 provides a new format - // that is not backwards-compatible with previous versions of IE. - p.platform = getPlatform(s.comment) - if p.platform == "Windows" && len(s.comment) > 0 { - p.os = normalizeOS(s.comment[0]) - } - - // And finally get the OS depending on the engine. - switch p.browser.Engine { - case "": - p.undecided = true - case "Gecko": - gecko(p, s.comment) - case "AppleWebKit": - webkit(p, s.comment) - case "Trident": - trident(p, s.comment) - } - } else if s.name == "Opera" { - if len(s.comment) > 0 { - opera(p, s.comment) - } - } else if s.name == "Dalvik" { - if len(s.comment) > 0 { - dalvik(p, s.comment) - } - } else { - // Check whether this is a bot or just a weird browser. - p.undecided = true - } -} - -// Returns a string containing the platform.. -func (p *UserAgent) Platform() string { - return p.platform -} - -// Returns a string containing the name of the Operating System. -func (p *UserAgent) OS() string { - return p.os -} - -// Returns a string containing the localization. -func (p *UserAgent) Localization() string { - return p.localization -} - -// Return OS name and version from a slice of strings created from the full name of the OS. -func osName(osSplit []string) (name, version string) { - if len(osSplit) == 1 { - name = osSplit[0] - version = "" - } else { - // Assume version is stored in the last part of the array. - nameSplit := osSplit[:len(osSplit)-1] - version = osSplit[len(osSplit)-1] - - // Nicer looking Mac OS X - if len(nameSplit) >= 2 && nameSplit[0] == "Intel" && nameSplit[1] == "Mac" { - nameSplit = nameSplit[1:] - } - name = strings.Join(nameSplit, " ") - - if strings.Contains(version, "x86") || strings.Contains(version, "i686") { - // x86_64 and i868 are not Linux versions but architectures - version = "" - } else if version == "X" && name == "Mac OS" { - // X is not a version for Mac OS. - name = name + " " + version - version = "" - } - } - return name, version -} - -// Returns combined information for the operating system. -func (p *UserAgent) OSInfo() OSInfo { - // Special case for iPhone weirdness - os := strings.Replace(p.os, "like Mac OS X", "", 1) - os = strings.Replace(os, "CPU", "", 1) - os = strings.Trim(os, " ") - - osSplit := strings.Split(os, " ") - - // Special case for x64 edition of Windows - if os == "Windows XP x64 Edition" { - osSplit = osSplit[:len(osSplit)-2] - } - - name, version := osName(osSplit) - - // Special case for names that contain a forward slash version separator. - if strings.Contains(name, "/") { - s := strings.Split(name, "/") - name = s[0] - version = s[1] - } - - // Special case for versions that use underscores - version = strings.Replace(version, "_", ".", -1) - - return OSInfo{ - FullName: p.os, - Name: name, - Version: version, - } -} diff --git a/vendor/github.com/mssola/user_agent/user_agent.go b/vendor/github.com/mssola/user_agent/user_agent.go deleted file mode 100644 index 436e94980..000000000 --- a/vendor/github.com/mssola/user_agent/user_agent.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com> -// This file is licensed under the MIT license. -// See the LICENSE file. - -// Package user_agent implements an HTTP User Agent string parser. It defines -// the type UserAgent that contains all the information from the parsed string. -// It also implements the Parse function and getters for all the relevant -// information that has been extracted from a parsed User Agent string. -package user_agent - -import "strings" - -// A section contains the name of the product, its version and -// an optional comment. -type section struct { - name string - version string - comment []string -} - -// The UserAgent struct contains all the info that can be extracted -// from the User-Agent string. -type UserAgent struct { - ua string - mozilla string - platform string - os string - localization string - browser Browser - bot bool - mobile bool - undecided bool -} - -// Read from the given string until the given delimiter or the -// end of the string have been reached. -// -// The first argument is the user agent string being parsed. The second -// argument is a reference pointing to the current index of the user agent -// string. The delimiter argument specifies which character is the delimiter -// and the cat argument determines whether nested '(' should be ignored or not. -// -// Returns an array of bytes containing what has been read. -func readUntil(ua string, index *int, delimiter byte, cat bool) []byte { - var buffer []byte - - i := *index - catalan := 0 - for ; i < len(ua); i = i + 1 { - if ua[i] == delimiter { - if catalan == 0 { - *index = i + 1 - return buffer - } - catalan-- - } else if cat && ua[i] == '(' { - catalan++ - } - buffer = append(buffer, ua[i]) - } - *index = i + 1 - return buffer -} - -// Parse the given product, that is, just a name or a string -// formatted as Name/Version. -// -// It returns two strings. The first string is the name of the product and the -// second string contains the version of the product. -func parseProduct(product []byte) (string, string) { - prod := strings.SplitN(string(product), "/", 2) - if len(prod) == 2 { - return prod[0], prod[1] - } - return string(product), "" -} - -// Parse a section. A section is typically formatted as follows -// "Name/Version (comment)". Both, the comment and the version are optional. -// -// The first argument is the user agent string being parsed. The second -// argument is a reference pointing to the current index of the user agent -// string. -// -// Returns a section containing the information that we could extract -// from the last parsed section. -func parseSection(ua string, index *int) (s section) { - buffer := readUntil(ua, index, ' ', false) - - s.name, s.version = parseProduct(buffer) - if *index < len(ua) && ua[*index] == '(' { - *index++ - buffer = readUntil(ua, index, ')', true) - s.comment = strings.Split(string(buffer), "; ") - *index++ - } - return s -} - -// Initialize the parser. -func (p *UserAgent) initialize() { - p.ua = "" - p.mozilla = "" - p.platform = "" - p.os = "" - p.localization = "" - p.browser.Engine = "" - p.browser.EngineVersion = "" - p.browser.Name = "" - p.browser.Version = "" - p.bot = false - p.mobile = false - p.undecided = false -} - -// Parse the given User-Agent string and get the resulting UserAgent object. -// -// Returns an UserAgent object that has been initialized after parsing -// the given User-Agent string. -func New(ua string) *UserAgent { - o := &UserAgent{} - o.Parse(ua) - return o -} - -// Parse the given User-Agent string. After calling this function, the -// receiver will be setted up with all the information that we've extracted. -func (p *UserAgent) Parse(ua string) { - var sections []section - - p.initialize() - p.ua = ua - for index, limit := 0, len(ua); index < limit; { - s := parseSection(ua, &index) - if !p.mobile && s.name == "Mobile" { - p.mobile = true - } - sections = append(sections, s) - } - - if len(sections) > 0 { - if sections[0].name == "Mozilla" { - p.mozilla = sections[0].version - } - - p.detectBrowser(sections) - p.detectOS(sections[0]) - - if p.undecided { - p.checkBot(sections) - } - } -} - -// Returns the mozilla version (it's how the User Agent string begins: -// "Mozilla/5.0 ...", unless we're dealing with Opera, of course). -func (p *UserAgent) Mozilla() string { - return p.mozilla -} - -// Returns true if it's a bot, false otherwise. -func (p *UserAgent) Bot() bool { - return p.bot -} - -// Returns true if it's a mobile device, false otherwise. -func (p *UserAgent) Mobile() bool { - return p.mobile -} - -// Returns the original given user agent. -func (p *UserAgent) UA() string { - return p.ua -} diff --git a/web/web.go b/web/web.go index e0edd1b7a..22fe43923 100644 --- a/web/web.go +++ b/web/web.go @@ -8,12 +8,12 @@ import ( "strings" "github.com/NYTimes/gziphandler" + "github.com/avct/uasurfer" l4g "github.com/alecthomas/log4go" "github.com/mattermost/mattermost-server/api" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" - "github.com/mssola/user_agent" ) func Init(api3 *api.API) { @@ -65,26 +65,27 @@ func pluginHandler(config model.ConfigFunc, handler http.Handler) http.Handler { }) } -var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" +// Due to the complexities of UA detection and the ramifications of a misdetection only older Safari and IE browsers throw incompatibility errors. -func CheckBrowserCompatability(c *api.Context, r *http.Request) bool { - ua := user_agent.New(r.UserAgent()) - bname, bversion := ua.Browser() +// Map should be of minimum required browser version. +var browserMinimumSupported = map[string]int{ + "BrowserIE": 11, + "BrowserSafari": 9, +} - browsers := strings.Split(browsersNotSupported, ";") - for _, browser := range browsers { - version := strings.Split(browser, "/") +func CheckClientCompatability(agentString string) bool { + ua := uasurfer.Parse(agentString) - if strings.HasPrefix(bname, version[0]) && strings.HasPrefix(bversion, version[1]) { - return false - } + if version, exist := browserMinimumSupported[ua.Browser.Name.String()]; exist && ua.Browser.Version.Major < version { + return false } return true } func root(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { + + if !CheckClientCompatability(r.UserAgent()) { w.Header().Set("Cache-Control", "no-store") page := utils.NewHTMLTemplate(c.App.HTMLTemplates(), "unsupported_browser") page.Props["Title"] = c.T("web.error.unsupported_browser.title") diff --git a/web/web_test.go b/web/web_test.go index 373d47103..7da16ab24 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -166,4 +166,35 @@ func TestMain(m *testing.M) { }() status = m.Run() + +} + +func TestCheckClientCompatability(t *testing.T) { + //Browser Name, UA String, expected result (if the browser should fail the test false and if it should pass the true) + type uaTest struct { + Name string // Name of Browser + UserAgent string // Useragent of Browser + Result bool // Expected result (true if browser should be compatible, false if browser shouldn't be compatible) + } + var uaTestParameters = []uaTest{ + {"Mozilla 40.1", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", true}, + {"Chrome 60", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", true}, + {"Chrome Mobile", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Mobile Safari/537.36", true}, + {"MM Classic App", "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR6.170623.013; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/61.0.3163.81 Mobile Safari/537.36 Web-Atoms-Mobile-WebView", true}, + {"MM App 3.7.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Mattermost/3.7.1 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36", true}, + {"Franz 4.0.4", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/4.0.4 Chrome/52.0.2743.82 Electron/1.3.1 Safari/537.36", true}, + {"Edge 14", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393", true}, + {"Internet Explorer 11", "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", true}, + {"Internet Explorer 9", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0", false}, + {"Safari 9", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38", true}, + {"Safari 8", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12", false}, + {"Safari Mobile", "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B137 Safari/601.1", true}, + } + for _, browser := range uaTestParameters { + t.Run(browser.Name, func(t *testing.T) { + if result := CheckClientCompatability(browser.UserAgent); result != browser.Result { + t.Fatalf("%s User Agent Test failed!", browser.Name) + } + }) + } } diff --git a/wsapi/user.go b/wsapi/user.go index 68fe27e0f..509ca8a14 100644 --- a/wsapi/user.go +++ b/wsapi/user.go @@ -29,7 +29,7 @@ func (api *API) userTyping(req *model.WebSocketRequest) (map[string]interface{}, event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers) event.Add("parent_id", parentId) event.Add("user_id", req.Session.UserId) - go api.App.Publish(event) + api.App.Publish(event) return nil, nil } diff --git a/wsapi/webrtc.go b/wsapi/webrtc.go index fbefb1b38..de50fa06b 100644 --- a/wsapi/webrtc.go +++ b/wsapi/webrtc.go @@ -20,7 +20,7 @@ func (api *API) webrtcMessage(req *model.WebSocketRequest) (map[string]interface event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_WEBRTC, "", "", toUserId, nil) event.Data = req.Data - go api.App.Publish(event) + api.App.Publish(event) return nil, nil } |