summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--api/channel_test.go15
-rw-r--r--api/context.go13
-rw-r--r--api/post.go5
-rw-r--r--api/websocket_test.go2
-rw-r--r--api4/apitestlib.go27
-rw-r--r--api4/channel_test.go7
-rw-r--r--api4/system.go41
-rw-r--r--api4/system_test.go64
-rw-r--r--api4/team.go83
-rw-r--r--api4/team_test.go81
-rw-r--r--api4/webhook.go1
-rw-r--r--app/admin.go10
-rw-r--r--app/app.go18
-rw-r--r--app/apptestlib.go8
-rw-r--r--app/authorization.go12
-rw-r--r--app/auto_constants.go22
-rw-r--r--app/auto_posts.go15
-rw-r--r--app/channel.go45
-rw-r--r--app/channel_test.go69
-rw-r--r--app/command_expand_collapse.go4
-rw-r--r--app/config.go15
-rw-r--r--app/config_test.go10
-rw-r--r--app/emoji.go1
-rw-r--r--app/post.go18
-rw-r--r--app/preference.go8
-rw-r--r--app/server.go47
-rw-r--r--app/session.go3
-rw-r--r--app/slackimport.go12
-rw-r--r--app/status.go16
-rw-r--r--app/team.go99
-rw-r--r--app/user.go10
-rw-r--r--build/release.mk18
-rw-r--r--cmd/cmd.go26
-rw-r--r--cmd/cmdtestlib.go (renamed from cmd/platform/platform_test.go)14
-rw-r--r--cmd/commands/channel.go (renamed from cmd/platform/channel.go)176
-rw-r--r--cmd/commands/channel_test.go (renamed from cmd/platform/channel_test.go)33
-rw-r--r--cmd/commands/channelargs.go (renamed from cmd/platform/channelargs.go)3
-rw-r--r--cmd/commands/command.go (renamed from cmd/platform/command.go)25
-rw-r--r--cmd/commands/commandargs.go (renamed from cmd/platform/commandargs.go)3
-rw-r--r--cmd/commands/config.go (renamed from cmd/platform/config.go)19
-rw-r--r--cmd/commands/config_flag_test.go (renamed from cmd/platform/mattermost_test.go)11
-rw-r--r--cmd/commands/config_test.go (renamed from cmd/platform/config_test.go)7
-rw-r--r--cmd/commands/exec_command_test.go21
-rw-r--r--cmd/commands/import.go (renamed from cmd/platform/import.go)61
-rw-r--r--cmd/commands/jobserver.go (renamed from cmd/platform/jobserver.go)20
-rw-r--r--cmd/commands/ldap.go (renamed from cmd/platform/ldap.go)21
-rw-r--r--cmd/commands/license.go (renamed from cmd/platform/license.go)17
-rw-r--r--cmd/commands/message_export.go (renamed from cmd/platform/message_export.go)26
-rw-r--r--cmd/commands/message_export_test.go (renamed from cmd/platform/message_export_test.go)11
-rw-r--r--cmd/commands/reset.go53
-rw-r--r--cmd/commands/roles.go (renamed from cmd/platform/roles.go)25
-rw-r--r--cmd/commands/roles_test.go (renamed from cmd/platform/roles_test.go)5
-rw-r--r--cmd/commands/sampledata.go (renamed from cmd/platform/sampledata.go)82
-rw-r--r--cmd/commands/sampledata_test.go (renamed from cmd/platform/sampledata_test.go)9
-rw-r--r--cmd/commands/server.go (renamed from cmd/platform/server.go)16
-rw-r--r--cmd/commands/server_test.go (renamed from cmd/platform/server_test.go)4
-rw-r--r--cmd/commands/team.go (renamed from cmd/platform/team.go)83
-rw-r--r--cmd/commands/team_test.go (renamed from cmd/platform/team_test.go)9
-rw-r--r--cmd/commands/teamargs.go (renamed from cmd/platform/teamargs.go)3
-rw-r--r--cmd/commands/test.go (renamed from cmd/platform/test.go)38
-rw-r--r--cmd/commands/user.go (renamed from cmd/platform/user.go)219
-rw-r--r--cmd/commands/user_test.go (renamed from cmd/platform/user_test.go)15
-rw-r--r--cmd/commands/userargs.go (renamed from cmd/platform/userargs.go)3
-rw-r--r--cmd/commands/version.go (renamed from cmd/platform/version.go)26
-rw-r--r--cmd/commands/version_test.go (renamed from cmd/platform/version_test.go)6
-rw-r--r--cmd/init.go (renamed from cmd/platform/init.go)8
-rw-r--r--cmd/output.go (renamed from cmd/platform/output.go)3
-rw-r--r--cmd/platform/mattermost.go88
-rw-r--r--einterfaces/metrics.go2
-rw-r--r--glide.lock6
-rw-r--r--glide.yaml1
-rw-r--r--i18n/en.json80
-rw-r--r--jobs/testworker.go106
-rw-r--r--main.go36
-rw-r--r--model/authorization.go13
-rw-r--r--model/channel.go5
-rw-r--r--model/client4.go67
-rw-r--r--model/cluster_info.go6
-rw-r--r--model/config.go66
-rw-r--r--model/config_test.go120
-rw-r--r--model/emoji.go4
-rw-r--r--model/ldap.go1
-rw-r--r--model/manifest.go9
-rw-r--r--model/manifest_test.go6
-rw-r--r--model/oauth.go11
-rw-r--r--model/saml.go3
-rw-r--r--model/search_params.go6
-rw-r--r--model/team.go36
-rw-r--r--model/user.go6
-rw-r--r--model/utils.go15
-rw-r--r--model/version.go4
-rw-r--r--model/websocket_message.go2
-rwxr-xr-xscripts/prereq-check.sh4
-rw-r--r--store/layered_store_supplier.go2
-rw-r--r--store/sqlstore/channel_store.go53
-rw-r--r--store/sqlstore/compliance_store.go6
-rw-r--r--store/sqlstore/file_info_store.go8
-rw-r--r--store/sqlstore/post_store.go12
-rw-r--r--store/sqlstore/team_store.go11
-rw-r--r--store/sqlstore/upgrade.go10
-rw-r--r--store/sqlstore/user_store.go17
-rw-r--r--store/sqlstore/webhook_store.go15
-rw-r--r--store/store.go6
-rw-r--r--store/storetest/compliance_store.go298
-rw-r--r--store/storetest/mocks/ChannelStore.go5
-rw-r--r--store/storetest/mocks/FileInfoStore.go5
-rw-r--r--store/storetest/mocks/PostStore.go5
-rw-r--r--store/storetest/mocks/TeamStore.go16
-rw-r--r--store/storetest/mocks/UserStore.go5
-rw-r--r--store/storetest/mocks/WebhookStore.go5
-rw-r--r--store/storetest/team_store.go26
-rw-r--r--templates/unsupported_browser.html2
-rw-r--r--utils/config.go3
-rw-r--r--utils/file_backend_s3.go21
-rw-r--r--utils/file_backend_s3_test.go32
-rw-r--r--utils/i18n.go5
-rw-r--r--utils/log.go33
-rw-r--r--utils/logger/log4go_json_writer.go30
-rw-r--r--utils/logger/logger.go222
-rw-r--r--utils/lru.go113
-rw-r--r--utils/lru_test.go33
-rw-r--r--utils/mail.go64
-rw-r--r--utils/mail_test.go64
-rw-r--r--vendor/github.com/avct/uasurfer/.gitignore56
-rw-r--r--vendor/github.com/avct/uasurfer/.travis.yml11
-rw-r--r--vendor/github.com/avct/uasurfer/LICENSE192
-rw-r--r--vendor/github.com/avct/uasurfer/README.md169
-rw-r--r--vendor/github.com/avct/uasurfer/browser.go192
-rw-r--r--vendor/github.com/avct/uasurfer/const_string.go49
-rw-r--r--vendor/github.com/avct/uasurfer/device.go60
-rw-r--r--vendor/github.com/avct/uasurfer/system.go336
-rw-r--r--vendor/github.com/avct/uasurfer/uasurfer.go227
-rw-r--r--vendor/github.com/avct/uasurfer/uasurfer_test.go1074
-rw-r--r--web/web.go25
-rw-r--r--web/web_test.go31
-rw-r--r--wsapi/user.go2
-rw-r--r--wsapi/webrtc.go2
138 files changed, 4632 insertions, 1526 deletions
diff --git a/Makefile b/Makefile
index 490522c06..c4c0dc68f 100644
--- a/Makefile
+++ b/Makefile
@@ -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
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..2bb80ddfb 100644
--- a/api4/apitestlib.go
+++ b/api4/apitestlib.go
@@ -510,18 +510,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 +657,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/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/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..9e5bfc637 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()
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/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/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/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..d677f9a23 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) {
@@ -247,9 +245,7 @@ func (a *App) SetStatusOffline(userId string, manual bool) {
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.Publish(event)
}
func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) {
@@ -290,9 +286,7 @@ func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) {
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.Publish(event)
}
func (a *App) SetStatusDoNotDisturb(userId string) {
@@ -318,9 +312,7 @@ func (a *App) SetStatusDoNotDisturb(userId string) {
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
event.Add("status", model.STATUS_DND)
event.Add("user_id", status.UserId)
- a.Go(func() {
- a.Publish(event)
- })
+ a.Publish(event)
}
func GetStatusFromCache(userId string) *model.Status {
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..c303cbc68 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
}
@@ -832,7 +829,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)
}
@@ -1002,9 +998,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/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 e2a8c9748..dda1c5bfe 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,7 @@ var resetUserPasswordCmd = &cobra.Command{
RunE: resetUserPasswordCmdF,
}
-var resetUserMfaCmd = &cobra.Command{
+var ResetUserMfaCmd = &cobra.Command{
Use: "resetmfa [users]",
Short: "Turn off MFA",
Long: `Turn off multi-factor authentication for a user.
@@ -73,7 +75,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 +83,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 +91,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 +129,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 +137,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 +146,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 +186,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 +223,24 @@ 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,
+ 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 +259,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 +278,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 +292,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 +332,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 +357,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 +371,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 +399,8 @@ func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func resetUserMfaCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func resetUserMfaCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -421,8 +424,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 +434,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 +465,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 +475,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 +495,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 +528,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 +573,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 +591,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 +611,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 +636,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..960ac3878 100644
--- a/cmd/platform/user_test.go
+++ b/cmd/commands/user_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,9 +19,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 +47,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 +63,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 +75,8 @@ 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)
}
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/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..4e9183814 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:
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..1d71d7b3e 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")
}
@@ -2092,6 +2096,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 +3318,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/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/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+_~\-\.%=&amp;]*)?)?(?:#[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..c6d353959 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 {
diff --git a/store/sqlstore/compliance_store.go b/store/sqlstore/compliance_store.go
index 03d92d5e1..121d80102 100644
--- a/store/sqlstore/compliance_store.go
+++ b/store/sqlstore/compliance_store.go
@@ -223,7 +223,11 @@ 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,
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..04e0c994c 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,6 +403,9 @@ 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 {
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..0394277b7 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 {
@@ -160,6 +161,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 +191,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,6 +212,7 @@ 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
@@ -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/compliance_store.go b/store/storetest/compliance_store.go
index eb29bedc7..0a6b79cc5 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
@@ -428,13 +522,175 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
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..4ed3e39fb 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)
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..8a7f030dc 100644
--- a/store/storetest/mocks/TeamStore.go
+++ b/store/storetest/mocks/TeamStore.go
@@ -476,3 +476,19 @@ func (_m *TeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel {
return r0
}
+
+// UpdateLastTeamIconUpdate provides a mock function with given fields: teamId
+func (_m *TeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) store.StoreChannel {
+ ret := _m.Called(teamId)
+
+ 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
+}
diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go
index 7d1fd8c38..2f921ae6e 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)
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/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; en­us; 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/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..890b1ba58 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 {
+ if result := CheckClientCompatability(browser.UserAgent); result == browser.Result {
+ t.Logf("Pass: %s passed browser test.", browser.Name)
+ } else {
+ t.Errorf("Fail: %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
}