summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--api/admin.go7
-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.go43
-rw-r--r--api4/channel_test.go3
-rw-r--r--api4/compliance.go7
-rw-r--r--api4/system.go10
-rw-r--r--api4/user.go14
-rw-r--r--api4/user_test.go143
-rw-r--r--api4/webhook.go1
-rw-r--r--app/admin.go10
-rw-r--r--app/app.go18
-rw-r--r--app/apptestlib.go24
-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/diagnostics.go14
-rw-r--r--app/emoji.go1
-rw-r--r--app/login.go14
-rw-r--r--app/post.go18
-rw-r--r--app/preference.go8
-rw-r--r--app/server.go47
-rw-r--r--app/server_test.go2
-rw-r--r--app/session.go3
-rw-r--r--app/slackimport.go12
-rw-r--r--app/status.go44
-rw-r--r--app/status_test.go40
-rw-r--r--app/team.go9
-rw-r--r--app/user.go31
-rw-r--r--app/user_test.go129
-rw-r--r--app/webhook.go2
-rw-r--r--app/webhook_test.go58
-rw-r--r--build/Jenkinsfile2
-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)259
-rw-r--r--cmd/commands/user_test.go (renamed from cmd/platform/user_test.go)43
-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--config/default.json8
-rw-r--r--einterfaces/metrics.go2
-rw-r--r--glide.lock8
-rw-r--r--glide.yaml1
-rw-r--r--i18n/de.json26
-rw-r--r--i18n/en.json48
-rw-r--r--i18n/es.json26
-rw-r--r--i18n/fr.json26
-rw-r--r--i18n/it.json26
-rw-r--r--i18n/ja.json28
-rw-r--r--i18n/ko.json26
-rw-r--r--i18n/nl.json26
-rw-r--r--i18n/pl.json26
-rw-r--r--i18n/pt-BR.json26
-rw-r--r--i18n/ru.json26
-rw-r--r--i18n/tr.json44
-rw-r--r--i18n/zh-CN.json30
-rw-r--r--i18n/zh-TW.json26
-rw-r--r--jobs/testworker.go106
-rw-r--r--main.go36
-rw-r--r--model/channel.go5
-rw-r--r--model/client4.go15
-rw-r--r--model/cluster_info.go6
-rw-r--r--model/compliance_post.go3
-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.go14
-rw-r--r--model/manifest_test.go6
-rw-r--r--model/message_export.go1
-rw-r--r--model/oauth.go11
-rw-r--r--model/saml.go3
-rw-r--r--model/search_params.go6
-rw-r--r--model/team.go9
-rw-r--r--model/user.go6
-rw-r--r--model/utils.go15
-rw-r--r--model/version.go5
-rw-r--r--model/websocket_message.go2
-rw-r--r--plugin/pluginenv/environment.go21
-rw-r--r--plugin/rpcplugin/api.go2
-rw-r--r--plugin/rpcplugin/api_test.go43
-rw-r--r--plugin/rpcplugin/sandbox/sandbox_linux.go9
-rw-r--r--store/layered_store_supplier.go2
-rw-r--r--store/sqlstore/channel_store.go60
-rw-r--r--store/sqlstore/compliance_store.go9
-rw-r--r--store/sqlstore/file_info_store.go8
-rw-r--r--store/sqlstore/post_store.go12
-rw-r--r--store/sqlstore/upgrade.go14
-rw-r--r--store/sqlstore/user_store.go66
-rw-r--r--store/sqlstore/webhook_store.go15
-rw-r--r--store/store.go7
-rw-r--r--store/storetest/channel_store.go49
-rw-r--r--store/storetest/compliance_store.go300
-rw-r--r--store/storetest/mocks/ChannelStore.go21
-rw-r--r--store/storetest/mocks/FileInfoStore.go5
-rw-r--r--store/storetest/mocks/PostStore.go5
-rw-r--r--store/storetest/mocks/TeamStore.go20
-rw-r--r--store/storetest/mocks/UserStore.go21
-rw-r--r--store/storetest/mocks/WebhookStore.go5
-rw-r--r--store/storetest/user_store.go77
-rw-r--r--templates/unsupported_browser.html2
-rw-r--r--utils/config.go3
-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/mail.go6
-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--vendor/github.com/mssola/user_agent/.travis.yml13
-rw-r--r--vendor/github.com/mssola/user_agent/LICENSE20
-rw-r--r--vendor/github.com/mssola/user_agent/README.md51
-rw-r--r--vendor/github.com/mssola/user_agent/all_test.go594
-rw-r--r--vendor/github.com/mssola/user_agent/bot.go123
-rw-r--r--vendor/github.com/mssola/user_agent/browser.go140
-rw-r--r--vendor/github.com/mssola/user_agent/operating_systems.go359
-rw-r--r--vendor/github.com/mssola/user_agent/user_agent.go174
-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
167 files changed, 5007 insertions, 3042 deletions
diff --git a/Makefile b/Makefile
index 490522c06..451581dd3 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
@@ -118,7 +118,7 @@ start-docker: ## Starts the docker containers for local development.
@if [ $(shell docker ps -a | grep -ci mattermost-inbucket) -eq 0 ]; then \
echo starting mattermost-inbucket; \
- docker run --name mattermost-inbucket -p 9000:10080 -p 2500:10025 -d jhillyerd/inbucket:latest > /dev/null; \
+ docker run --name mattermost-inbucket -p 9000:10080 -p 2500:10025 -d jhillyerd/inbucket:release-1.2.0 > /dev/null; \
elif [ $(shell docker ps | grep -ci mattermost-inbucket) -eq 0 ]; then \
echo restarting mattermost-inbucket; \
docker start mattermost-inbucket > /dev/null; \
diff --git a/api/admin.go b/api/admin.go
index 3b58650cc..6016e48f3 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -7,10 +7,10 @@ import (
"net/http"
"strconv"
+ "github.com/avct/uasurfer"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/model"
- "github.com/mssola/user_agent"
)
func (api *API) InitAdmin() {
@@ -201,12 +201,11 @@ func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
// attach extra headers to trigger a download on IE, Edge, and Safari
- ua := user_agent.New(r.UserAgent())
- bname, _ := ua.Browser()
+ ua := uasurfer.Parse(r.UserAgent())
w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
- if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" {
+ if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari {
// trim off anything before the final / so we just get the file's name
w.Header().Set("Content-Type", "application/octet-stream")
}
diff --git a/api/channel_test.go b/api/channel_test.go
index 37dde24bd..2642eb9ff 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) {
@@ -345,7 +344,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")
@@ -356,7 +355,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)
@@ -508,7 +507,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")
@@ -519,7 +518,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)
@@ -629,7 +628,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")
@@ -640,7 +639,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)
@@ -1154,7 +1153,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 a788d4311..2de031f9b 100644
--- a/api4/apitestlib.go
+++ b/api4/apitestlib.go
@@ -468,6 +468,22 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
utils.EnableDebugLogForTest()
}
+func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
+ utils.DisableDebugLogForTest()
+
+ member, err := me.App.AddUserToChannel(user, channel)
+ if err != nil {
+ l4g.Error(err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(err)
+ }
+
+ utils.EnableDebugLogForTest()
+
+ return member
+}
+
func (me *TestHelper) GenerateTestEmail() string {
if me.App.Config().EmailSettings.SMTPServer != "dockerhost" && os.Getenv("CI_INBUCKET_PORT") == "" {
return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com")
@@ -511,18 +527,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()
@@ -670,21 +674,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 1b74ea880..b9ee5bc7d 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) {
@@ -874,7 +873,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)
diff --git a/api4/compliance.go b/api4/compliance.go
index 71f0fa81d..4035afb77 100644
--- a/api4/compliance.go
+++ b/api4/compliance.go
@@ -7,8 +7,8 @@ import (
"net/http"
"strconv"
+ "github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/model"
- "github.com/mssola/user_agent"
)
func (api *API) InitCompliance() {
@@ -108,12 +108,11 @@ func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
// attach extra headers to trigger a download on IE, Edge, and Safari
- ua := user_agent.New(r.UserAgent())
- bname, _ := ua.Browser()
+ ua := uasurfer.Parse(r.UserAgent())
w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
- if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" {
+ if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari {
// trim off anything before the final / so we just get the file's name
w.Header().Set("Content-Type", "application/octet-stream")
}
diff --git a/api4/system.go b/api4/system.go
index aab65bf20..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"
@@ -247,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) {
diff --git a/api4/user.go b/api4/user.go
index f82a6e3d5..8f8f08c75 100644
--- a/api4/user.go
+++ b/api4/user.go
@@ -290,16 +290,21 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if sort != "" && sort != "last_activity_at" && sort != "create_at" {
+ if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" {
c.SetInvalidUrlParam("sort")
return
}
// Currently only supports sorting on a team
+ // or sort="status" on inChannelId
if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") {
c.SetInvalidUrlParam("sort")
return
}
+ if sort == "status" && inChannelId == "" {
+ c.SetInvalidUrlParam("sort")
+ return
+ }
var profiles []*model.User
var err *model.AppError
@@ -355,8 +360,11 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
-
- profiles, err = c.App.GetUsersInChannelPage(inChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin())
+ if sort == "status" {
+ profiles, err = c.App.GetUsersInChannelPageByStatus(inChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin())
+ } else {
+ profiles, err = c.App.GetUsersInChannelPage(inChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin())
+ }
} else {
// No permission check required
diff --git a/api4/user_test.go b/api4/user_test.go
index 4613a8ea9..f04cd6ab2 100644
--- a/api4/user_test.go
+++ b/api4/user_test.go
@@ -2650,3 +2650,146 @@ func TestUserAccessTokenDisableConfig(t *testing.T) {
_, resp = Client.GetMe("")
CheckNoError(t, resp)
}
+
+func TestGetUsersByStatus(t *testing.T) {
+ th := Setup()
+ defer th.TearDown()
+
+ team, err := th.App.CreateTeam(&model.Team{
+ DisplayName: "dn_" + model.NewId(),
+ Name: GenerateTestTeamName(),
+ Email: th.GenerateTestEmail(),
+ Type: model.TEAM_OPEN,
+ })
+ if err != nil {
+ t.Fatalf("failed to create team: %v", err)
+ }
+
+ channel, err := th.App.CreateChannel(&model.Channel{
+ DisplayName: "dn_" + model.NewId(),
+ Name: "name_" + model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ TeamId: team.Id,
+ CreatorId: model.NewId(),
+ }, false)
+ if err != nil {
+ t.Fatalf("failed to create channel: %v", err)
+ }
+
+ createUserWithStatus := func(username string, status string) *model.User {
+ id := model.NewId()
+
+ user, err := th.App.CreateUser(&model.User{
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Username: "un_" + username + "_" + id,
+ Nickname: "nn_" + id,
+ Password: "Password1",
+ })
+ if err != nil {
+ t.Fatalf("failed to create user: %v", err)
+ }
+
+ th.LinkUserToTeam(user, team)
+ th.AddUserToChannel(user, channel)
+
+ th.App.SaveAndBroadcastStatus(&model.Status{
+ UserId: user.Id,
+ Status: status,
+ Manual: true,
+ })
+
+ return user
+ }
+
+ // Creating these out of order in case that affects results
+ offlineUser1 := createUserWithStatus("offline1", model.STATUS_OFFLINE)
+ offlineUser2 := createUserWithStatus("offline2", model.STATUS_OFFLINE)
+ awayUser1 := createUserWithStatus("away1", model.STATUS_AWAY)
+ awayUser2 := createUserWithStatus("away2", model.STATUS_AWAY)
+ onlineUser1 := createUserWithStatus("online1", model.STATUS_ONLINE)
+ onlineUser2 := createUserWithStatus("online2", model.STATUS_ONLINE)
+ dndUser1 := createUserWithStatus("dnd1", model.STATUS_DND)
+ dndUser2 := createUserWithStatus("dnd2", model.STATUS_DND)
+
+ client := th.CreateClient()
+ if _, resp := client.Login(onlineUser2.Username, "Password1"); resp.Error != nil {
+ t.Fatal(resp.Error)
+ }
+
+ t.Run("sorting by status then alphabetical", func(t *testing.T) {
+ usersByStatus, resp := client.GetUsersInChannelByStatus(channel.Id, 0, 8, "")
+ if resp.Error != nil {
+ t.Fatal(resp.Error)
+ }
+
+ expectedUsersByStatus := []*model.User{
+ onlineUser1,
+ onlineUser2,
+ awayUser1,
+ awayUser2,
+ dndUser1,
+ dndUser2,
+ offlineUser1,
+ offlineUser2,
+ }
+
+ if len(usersByStatus) != len(expectedUsersByStatus) {
+ t.Fatalf("received only %v users, expected %v", len(usersByStatus), len(expectedUsersByStatus))
+ }
+
+ for i := range usersByStatus {
+ if usersByStatus[i].Id != expectedUsersByStatus[i].Id {
+ t.Fatalf("received user %v at index %v, expected %v", usersByStatus[i].Username, i, expectedUsersByStatus[i].Username)
+ }
+ }
+ })
+
+ t.Run("paging", func(t *testing.T) {
+ usersByStatus, resp := client.GetUsersInChannelByStatus(channel.Id, 0, 3, "")
+ if resp.Error != nil {
+ t.Fatal(resp.Error)
+ }
+
+ if len(usersByStatus) != 3 {
+ t.Fatal("received too many users")
+ }
+
+ if usersByStatus[0].Id != onlineUser1.Id && usersByStatus[1].Id != onlineUser2.Id {
+ t.Fatal("expected to receive online users first")
+ }
+
+ if usersByStatus[2].Id != awayUser1.Id {
+ t.Fatal("expected to receive away users second")
+ }
+
+ usersByStatus, resp = client.GetUsersInChannelByStatus(channel.Id, 1, 3, "")
+ if resp.Error != nil {
+ t.Fatal(resp.Error)
+ }
+
+ if usersByStatus[0].Id != awayUser2.Id {
+ t.Fatal("expected to receive away users second")
+ }
+
+ if usersByStatus[1].Id != dndUser1.Id && usersByStatus[2].Id != dndUser2.Id {
+ t.Fatal("expected to receive dnd users third")
+ }
+
+ usersByStatus, resp = client.GetUsersInChannelByStatus(channel.Id, 1, 4, "")
+ if resp.Error != nil {
+ t.Fatal(resp.Error)
+ }
+
+ if len(usersByStatus) != 4 {
+ t.Fatal("received too many users")
+ }
+
+ if usersByStatus[0].Id != dndUser1.Id && usersByStatus[1].Id != dndUser2.Id {
+ t.Fatal("expected to receive dnd users third")
+ }
+
+ if usersByStatus[2].Id != offlineUser1.Id && usersByStatus[3].Id != offlineUser2.Id {
+ t.Fatal("expected to receive offline users last")
+ }
+ })
+}
diff --git a/api4/webhook.go b/api4/webhook.go
index dcbf6c2af..52c4ea331 100644
--- a/api4/webhook.go
+++ b/api4/webhook.go
@@ -522,7 +522,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 5bc418e0d..c4d11cb63 100644
--- a/app/app.go
+++ b/app/app.go
@@ -133,8 +133,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()
l4g.Info(utils.T("api.server.new_server.init.info"))
diff --git a/app/apptestlib.go b/app/apptestlib.go
index 3402f1f79..6c2273c6e 100644
--- a/app/apptestlib.go
+++ b/app/apptestlib.go
@@ -143,10 +143,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"
}
@@ -199,10 +195,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()
@@ -262,6 +254,22 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
utils.EnableDebugLogForTest()
}
+func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
+ utils.DisableDebugLogForTest()
+
+ member, err := me.App.AddUserToChannel(user, channel)
+ if err != nil {
+ l4g.Error(err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(err)
+ }
+
+ utils.EnableDebugLogForTest()
+
+ return member
+}
+
func (me *TestHelper) TearDown() {
me.App.Shutdown()
os.Remove(me.tempConfigPath)
diff --git a/app/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 af36774de..cd694af0f 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{
@@ -549,7 +557,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)
}
@@ -1059,7 +1066,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
}
@@ -1097,7 +1104,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,
},
}
@@ -1117,7 +1126,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,
},
}
@@ -1136,6 +1147,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,
},
}
@@ -1170,17 +1182,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
}
@@ -1250,9 +1258,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)
}
}
@@ -1329,9 +1335,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
@@ -1434,7 +1438,16 @@ func (a *App) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.
}
a.InvalidateCacheForUser(userId1)
a.InvalidateCacheForUser(userId2)
- return result.Data.(*model.Channel), nil
+
+ channel := result.Data.(*model.Channel)
+ if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId1, channel.Id, model.GetMillis()); result.Err != nil {
+ l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err)
+ }
+ if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId2, channel.Id, model.GetMillis()); result.Err != nil {
+ l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err)
+ }
+
+ return channel, nil
} else if result.Err != nil {
return nil, model.NewAppError("GetOrCreateDMChannel", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode)
}
diff --git a/app/channel_test.go b/app/channel_test.go
index e4a0e4320..69efaeca7 100644
--- a/app/channel_test.go
+++ b/app/channel_test.go
@@ -110,7 +110,7 @@ func TestMoveChannel(t *testing.T) {
}
}
-func TestJoinDefaultChannelsTownSquare(t *testing.T) {
+func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordTownSquare(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
@@ -136,7 +136,7 @@ func TestJoinDefaultChannelsTownSquare(t *testing.T) {
assert.True(t, found)
}
-func TestJoinDefaultChannelsOffTopic(t *testing.T) {
+func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordOffTopic(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
@@ -162,7 +162,7 @@ func TestJoinDefaultChannelsOffTopic(t *testing.T) {
assert.True(t, found)
}
-func TestCreateChannelPublic(t *testing.T) {
+func TestCreateChannelPublicCreatesChannelMemberHistoryRecord(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
@@ -176,7 +176,7 @@ func TestCreateChannelPublic(t *testing.T) {
assert.Equal(t, publicChannel.Id, histories[0].ChannelId)
}
-func TestCreateChannelPrivate(t *testing.T) {
+func TestCreateChannelPrivateCreatesChannelMemberHistoryRecord(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
@@ -205,7 +205,7 @@ func TestUpdateChannelPrivacy(t *testing.T) {
}
}
-func TestCreateGroupChannel(t *testing.T) {
+func TestCreateGroupChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
@@ -233,7 +233,62 @@ func TestCreateGroupChannel(t *testing.T) {
}
}
-func TestAddUserToChannel(t *testing.T) {
+func TestCreateDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user1 := th.CreateUser()
+ user2 := th.CreateUser()
+
+ if channel, err := th.App.CreateDirectChannel(user1.Id, user2.Id); err != nil {
+ t.Fatal("Failed to create direct channel. Error: " + err.Message)
+ } else {
+ // there should be a ChannelMemberHistory record for both users
+ histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult)
+ assert.Len(t, histories, 2)
+
+ historyId0 := histories[0].UserId
+ historyId1 := histories[1].UserId
+ switch historyId0 {
+ case user1.Id:
+ assert.Equal(t, user2.Id, historyId1)
+ case user2.Id:
+ assert.Equal(t, user1.Id, historyId1)
+ default:
+ t.Fatal("Unexpected user id " + historyId0 + " in ChannelMemberHistory table")
+ }
+ }
+}
+
+func TestGetDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user1 := th.CreateUser()
+ user2 := th.CreateUser()
+
+ // this function call implicitly creates a direct channel between the two users if one doesn't already exist
+ if channel, err := th.App.GetDirectChannel(user1.Id, user2.Id); err != nil {
+ t.Fatal("Failed to create direct channel. Error: " + err.Message)
+ } else {
+ // there should be a ChannelMemberHistory record for both users
+ histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult)
+ assert.Len(t, histories, 2)
+
+ historyId0 := histories[0].UserId
+ historyId1 := histories[1].UserId
+ switch historyId0 {
+ case user1.Id:
+ assert.Equal(t, user2.Id, historyId1)
+ case user2.Id:
+ assert.Equal(t, user1.Id, historyId1)
+ default:
+ t.Fatal("Unexpected user id " + historyId0 + " in ChannelMemberHistory table")
+ }
+ }
+}
+
+func TestAddUserToChannelCreatesChannelMemberHistoryRecord(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
@@ -263,7 +318,7 @@ func TestAddUserToChannel(t *testing.T) {
assert.Equal(t, groupUserIds, channelMemberHistoryUserIds)
}
-func TestRemoveUserFromChannel(t *testing.T) {
+func TestRemoveUserFromChannelUpdatesChannelMemberHistoryRecord(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
diff --git a/app/command_expand_collapse.go b/app/command_expand_collapse.go
index a8eb3bc1f..638490c6c 100644
--- a/app/command_expand_collapse.go
+++ b/app/command_expand_collapse.go
@@ -74,9 +74,7 @@ func (a *App) setCollapsePreference(args *model.CommandArgs, isCollapse bool) *m
socketMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", args.UserId, nil)
socketMessage.Add("preference", pref.ToJson())
- a.Go(func() {
- a.Publish(socketMessage)
- })
+ a.Publish(socketMessage)
var rmsg string
diff --git a/app/config.go b/app/config.go
index 35a0c9a3f..460d580d8 100644
--- a/app/config.go
+++ b/app/config.go
@@ -14,6 +14,7 @@ import (
"fmt"
"net/url"
"runtime/debug"
+ "strconv"
"strings"
l4g "github.com/alecthomas/log4go"
@@ -34,6 +35,7 @@ func (a *App) UpdateConfig(f func(*model.Config)) {
updated := old.Clone()
f(updated)
a.config.Store(updated)
+
a.InvokeConfigListeners(old, updated)
}
@@ -269,3 +271,16 @@ func (a *App) GetCookieDomain() string {
func (a *App) GetSiteURL() string {
return a.siteURL
}
+
+// ClientConfigWithNoAccounts gets the configuration in a format suitable for sending to the client.
+func (a *App) ClientConfigWithNoAccounts() map[string]string {
+ respCfg := map[string]string{}
+ for k, v := range a.ClientConfig() {
+ respCfg[k] = v
+ }
+
+ // NoAccounts is not actually part of the configuration, but is expected by the client.
+ respCfg["NoAccounts"] = strconv.FormatBool(a.IsFirstUserAccount())
+
+ return respCfg
+}
diff --git a/app/config_test.go b/app/config_test.go
index 5ee999f0f..051fa8fd8 100644
--- a/app/config_test.go
+++ b/app/config_test.go
@@ -63,3 +63,13 @@ func TestAsymmetricSigningKey(t *testing.T) {
assert.NotNil(t, th.App.AsymmetricSigningKey())
assert.NotEmpty(t, th.App.ClientConfig()["AsymmetricSigningPublicKey"])
}
+
+func TestClientConfigWithNoAccounts(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ config := th.App.ClientConfigWithNoAccounts()
+ if _, ok := config["NoAccounts"]; !ok {
+ t.Fatal("expected NoAccounts in returned config")
+ }
+}
diff --git a/app/diagnostics.go b/app/diagnostics.go
index ddea3289f..b08c4f86b 100644
--- a/app/diagnostics.go
+++ b/app/diagnostics.go
@@ -502,11 +502,15 @@ func (a *App) trackConfig() {
})
a.SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{
- "enable_message_export": *cfg.MessageExportSettings.EnableExport,
- "export_format": *cfg.MessageExportSettings.ExportFormat,
- "daily_run_time": *cfg.MessageExportSettings.DailyRunTime,
- "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp,
- "batch_size": *cfg.MessageExportSettings.BatchSize,
+ "enable_message_export": *cfg.MessageExportSettings.EnableExport,
+ "export_format": *cfg.MessageExportSettings.ExportFormat,
+ "daily_run_time": *cfg.MessageExportSettings.DailyRunTime,
+ "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp,
+ "batch_size": *cfg.MessageExportSettings.BatchSize,
+ "global_relay_customer_type": *cfg.MessageExportSettings.GlobalRelaySettings.CustomerType,
+ "is_default_global_relay_smtp_username": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SmtpUsername, ""),
+ "is_default_global_relay_smtp_password": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SmtpPassword, ""),
+ "is_default_global_relay_email_address": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.EmailAddress, ""),
})
}
diff --git a/app/emoji.go b/app/emoji.go
index 20d4bb44d..eebe59ccf 100644
--- a/app/emoji.go
+++ b/app/emoji.go
@@ -60,7 +60,6 @@ func (a *App) CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartIma
} else {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EMOJI_ADDED, "", "", "", nil)
message.Add("emoji", emoji.ToJson())
-
a.Publish(message)
return result.Data.(*model.Emoji), nil
}
diff --git a/app/login.go b/app/login.go
index e01566bcd..43b022749 100644
--- a/app/login.go
+++ b/app/login.go
@@ -9,8 +9,8 @@ import (
"strings"
"time"
+ "github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/model"
- "github.com/mssola/user_agent"
)
func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId string, ldapOnly bool) (*model.User, *model.AppError) {
@@ -71,19 +71,19 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
session.SetExpireInDays(*a.Config().ServiceSettings.SessionLengthWebInDays)
}
- ua := user_agent.New(r.UserAgent())
+ ua := uasurfer.Parse(r.UserAgent())
- plat := ua.Platform()
+ plat := ua.OS.Platform.String()
if plat == "" {
plat = "unknown"
}
- os := ua.OS()
+ os := ua.OS.Name.String()
if os == "" {
os = "unknown"
}
- bname, bversion := ua.Browser()
+ bname := ua.Browser.Name.String()
if bname == "" {
bname = "unknown"
}
@@ -92,9 +92,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
bname = "Desktop App"
}
- if bversion == "" {
- bversion = "0.0"
- }
+ bversion := ua.Browser.Version
session.AddProp(model.SESSION_PROP_PLATFORM, plat)
session.AddProp(model.SESSION_PROP_OS, os)
diff --git a/app/post.go b/app/post.go
index 3f157672b..1d7bf974d 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
}
@@ -417,10 +412,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) {
@@ -560,11 +552,9 @@ func (a *App) DeletePost(postId string) (*model.Post, *model.AppError) {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil)
message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson())
+ a.Publish(message)
a.Go(func() {
- a.Publish(message)
- })
- a.Go(func() {
a.DeletePostFiles(post)
})
a.Go(func() {
diff --git a/app/preference.go b/app/preference.go
index 9ca1f474c..eb41992da 100644
--- a/app/preference.go
+++ b/app/preference.go
@@ -55,9 +55,7 @@ func (a *App) UpdatePreferences(userId string, preferences model.Preferences) *m
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCES_CHANGED, "", "", userId, nil)
message.Add("preferences", preferences.ToJson())
- a.Go(func() {
- a.Publish(message)
- })
+ a.Publish(message)
return nil
}
@@ -80,9 +78,7 @@ func (a *App) DeletePreferences(userId string, preferences model.Preferences) *m
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCES_DELETED, "", "", userId, nil)
message.Add("preferences", preferences.ToJson())
- a.Go(func() {
- a.Publish(message)
- })
+ a.Publish(message)
return nil
}
diff --git a/app/server.go b/app/server.go
index 93804a372..0c6c25ba5 100644
--- a/app/server.go
+++ b/app/server.go
@@ -84,28 +84,6 @@ func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second
-type VaryBy struct {
- useIP bool
- useAuth bool
-}
-
-func (m *VaryBy) Key(r *http.Request) string {
- key := ""
-
- if m.useAuth {
- token, tokenLocation := ParseAuthTokenFromRequest(r)
- if tokenLocation != TokenLocationNotFound {
- key += token
- } else if m.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP
- key += utils.GetIpAddress(r)
- }
- } else if m.useIP { // Only if Auth based is not enabed do we use a plain IP based
- key = utils.GetIpAddress(r)
- }
-
- return key
-}
-
func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) {
if r.Host == "" {
http.Error(w, "Not Found", http.StatusNotFound)
@@ -223,31 +201,6 @@ func (a *App) StartServer() error {
return nil
}
-type tcpKeepAliveListener struct {
- *net.TCPListener
-}
-
-func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
- tc, err := ln.AcceptTCP()
- if err != nil {
- return
- }
- tc.SetKeepAlive(true)
- tc.SetKeepAlivePeriod(3 * time.Minute)
- return tc, nil
-}
-
-func (a *App) Listen(addr string) (net.Listener, error) {
- if addr == "" {
- addr = ":http"
- }
- ln, err := net.Listen("tcp", addr)
- if err != nil {
- return nil, err
- }
- return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil
-}
-
func (a *App) StopServer() {
if a.Srv.Server != nil {
ctx, cancel := context.WithTimeout(context.Background(), TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN)
diff --git a/app/server_test.go b/app/server_test.go
index de358b976..94771a44e 100644
--- a/app/server_test.go
+++ b/app/server_test.go
@@ -26,7 +26,7 @@ func TestStartServerRateLimiterCriticalError(t *testing.T) {
// Attempt to use Rate Limiter with an invalid config
a.UpdateConfig(func(cfg *model.Config) {
- *cfg.RateLimitSettings.Enable = true
+ *cfg.RateLimitSettings.Enable = true
*cfg.RateLimitSettings.MaxBurst = -100
})
diff --git a/app/session.go b/app/session.go
index 459618439..88f52477f 100644
--- a/app/session.go
+++ b/app/session.go
@@ -138,6 +138,9 @@ func (a *App) ClearSessionCacheForUserSkipClusterSend(userId string) {
session := ts.(*model.Session)
if session.UserId == userId {
a.sessionCache.Remove(key)
+ if a.Metrics != nil {
+ a.Metrics.IncrementMemCacheInvalidationCounterSession()
+ }
}
}
}
diff --git a/app/slackimport.go b/app/slackimport.go
index 9d1b4cf9c..ed522671a 100644
--- a/app/slackimport.go
+++ b/app/slackimport.go
@@ -109,13 +109,11 @@ func SlackParseUsers(data io.Reader) ([]SlackUser, error) {
decoder := json.NewDecoder(data)
var users []SlackUser
- if err := decoder.Decode(&users); err != nil {
- // This actually returns errors that are ignored.
- // In this case it is erroring because of a null that Slack
- // introduced. So we just return the users here.
- return users, err
- }
- return users, nil
+ err := decoder.Decode(&users)
+ // This actually returns errors that are ignored.
+ // In this case it is erroring because of a null that Slack
+ // introduced. So we just return the users here.
+ return users, err
}
func SlackParsePosts(data io.Reader) ([]SlackPost, error) {
diff --git a/app/status.go b/app/status.go
index 1ef7aef0f..c8bff0d1a 100644
--- a/app/status.go
+++ b/app/status.go
@@ -221,9 +221,7 @@ func (a *App) BroadcastStatus(status *model.Status) {
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
event.Add("status", status.Status)
event.Add("user_id", status.UserId)
- a.Go(func() {
- a.Publish(event)
- })
+ a.Publish(event)
}
func (a *App) SetStatusOffline(userId string, manual bool) {
@@ -238,18 +236,7 @@ func (a *App) SetStatusOffline(userId string, manual bool) {
status = &model.Status{UserId: userId, Status: model.STATUS_OFFLINE, Manual: manual, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
- a.AddStatusCache(status)
-
- if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
- l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
- }
-
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
- event.Add("status", model.STATUS_OFFLINE)
- event.Add("user_id", status.UserId)
- a.Go(func() {
- a.Publish(event)
- })
+ a.SaveAndBroadcastStatus(status)
}
func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) {
@@ -281,18 +268,7 @@ func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) {
status.Manual = manual
status.ActiveChannel = ""
- a.AddStatusCache(status)
-
- if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
- l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
- }
-
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
- event.Add("status", model.STATUS_AWAY)
- event.Add("user_id", status.UserId)
- a.Go(func() {
- a.Publish(event)
- })
+ a.SaveAndBroadcastStatus(status)
}
func (a *App) SetStatusDoNotDisturb(userId string) {
@@ -309,18 +285,22 @@ func (a *App) SetStatusDoNotDisturb(userId string) {
status.Status = model.STATUS_DND
status.Manual = true
+ a.SaveAndBroadcastStatus(status)
+}
+
+func (a *App) SaveAndBroadcastStatus(status *model.Status) *model.AppError {
a.AddStatusCache(status)
if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
- l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
+ l4g.Error(utils.T("api.status.save_status.error"), status.UserId, result.Err)
}
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
- event.Add("status", model.STATUS_DND)
+ event.Add("status", status.Status)
event.Add("user_id", status.UserId)
- a.Go(func() {
- a.Publish(event)
- })
+ a.Publish(event)
+
+ return nil
}
func GetStatusFromCache(userId string) *model.Status {
diff --git a/app/status_test.go b/app/status_test.go
new file mode 100644
index 000000000..bf5736a48
--- /dev/null
+++ b/app/status_test.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func TestSaveStatus(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.BasicUser
+
+ for _, statusString := range []string{
+ model.STATUS_ONLINE,
+ model.STATUS_AWAY,
+ model.STATUS_DND,
+ model.STATUS_OFFLINE,
+ } {
+ t.Run(statusString, func(t *testing.T) {
+ status := &model.Status{
+ UserId: user.Id,
+ Status: statusString,
+ }
+
+ th.App.SaveAndBroadcastStatus(status)
+
+ after, err := th.App.GetStatus(user.Id)
+ if err != nil {
+ t.Fatalf("failed to get status after save: %v", err)
+ } else if after.Status != statusString {
+ t.Fatalf("failed to save status, got %v, expected %v", after.Status, statusString)
+ }
+ })
+ }
+}
diff --git a/app/team.go b/app/team.go
index a6e79e9a6..a7b32af33 100644
--- a/app/team.go
+++ b/app/team.go
@@ -139,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) {
@@ -182,10 +180,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) {
diff --git a/app/user.go b/app/user.go
index 2b160f9f5..1a444e123 100644
--- a/app/user.go
+++ b/app/user.go
@@ -34,7 +34,6 @@ const (
TOKEN_TYPE_PASSWORD_RECOVERY = "password_recovery"
TOKEN_TYPE_VERIFY_EMAIL = "verify_email"
PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour
- VERIFY_EMAIL_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour
IMAGE_PROFILE_PIXEL_DIMENSION = 128
)
@@ -202,9 +201,7 @@ func (a *App) CreateUser(user *model.User) (*model.User, *model.AppError) {
// This message goes to everyone, so the teamId, channelId and userId are irrelevant
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)
message.Add("user_id", ruser.Id)
- a.Go(func() {
- a.Publish(message)
- })
+ a.Publish(message)
return ruser, nil
}
@@ -508,6 +505,14 @@ func (a *App) GetUsersInChannel(channelId string, offset int, limit int) ([]*mod
}
}
+func (a *App) GetUsersInChannelByStatus(channelId string, offset int, limit int) ([]*model.User, *model.AppError) {
+ if result := <-a.Srv.Store.User().GetProfilesInChannelByStatus(channelId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.User), nil
+ }
+}
+
func (a *App) GetUsersInChannelMap(channelId string, offset int, limit int, asAdmin bool) (map[string]*model.User, *model.AppError) {
users, err := a.GetUsersInChannel(channelId, offset, limit)
if err != nil {
@@ -533,6 +538,15 @@ func (a *App) GetUsersInChannelPage(channelId string, page int, perPage int, asA
return a.sanitizeProfiles(users, asAdmin), nil
}
+func (a *App) GetUsersInChannelPageByStatus(channelId string, page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) {
+ users, err := a.GetUsersInChannelByStatus(channelId, page*perPage, perPage)
+ if err != nil {
+ return nil, err
+ }
+
+ return a.sanitizeProfiles(users, asAdmin), nil
+}
+
func (a *App) GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) ([]*model.User, *model.AppError) {
if result := <-a.Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit); result.Err != nil {
return nil, result.Err
@@ -832,7 +846,6 @@ func (a *App) SetProfileImageFromFile(userId string, file multipart.File) *model
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil)
message.Add("user", user)
-
a.Publish(message)
}
@@ -901,10 +914,6 @@ func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.A
}
}
- if extra := <-a.Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil {
- return nil, extra.Err
- }
-
ruser := result.Data.([2]*model.User)[0]
options := a.Config().GetSanitizeOptions()
options["passwordupdate"] = false
@@ -1002,9 +1011,7 @@ func (a *App) sendUpdatedUserEvent(user model.User, asAdmin bool) {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil)
message.Add("user", user)
- a.Go(func() {
- a.Publish(message)
- })
+ a.Publish(message)
}
func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
diff --git a/app/user_test.go b/app/user_test.go
index 38ff286b3..94052da61 100644
--- a/app/user_test.go
+++ b/app/user_test.go
@@ -299,3 +299,132 @@ func createGitlabUser(t *testing.T, a *App, email string, username string) (*mod
return user, gitlabUserObj
}
+
+func TestGetUsersByStatus(t *testing.T) {
+ th := Setup()
+ defer th.TearDown()
+
+ team := th.CreateTeam()
+ channel, err := th.App.CreateChannel(&model.Channel{
+ DisplayName: "dn_" + model.NewId(),
+ Name: "name_" + model.NewId(),
+ Type: model.CHANNEL_OPEN,
+ TeamId: team.Id,
+ CreatorId: model.NewId(),
+ }, false)
+ if err != nil {
+ t.Fatalf("failed to create channel: %v", err)
+ }
+
+ createUserWithStatus := func(username string, status string) *model.User {
+ id := model.NewId()
+
+ user, err := th.App.CreateUser(&model.User{
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Username: "un_" + username + "_" + id,
+ Nickname: "nn_" + id,
+ Password: "Password1",
+ })
+ if err != nil {
+ t.Fatalf("failed to create user: %v", err)
+ }
+
+ th.LinkUserToTeam(user, team)
+ th.AddUserToChannel(user, channel)
+
+ th.App.SaveAndBroadcastStatus(&model.Status{
+ UserId: user.Id,
+ Status: status,
+ Manual: true,
+ })
+
+ return user
+ }
+
+ // Creating these out of order in case that affects results
+ awayUser1 := createUserWithStatus("away1", model.STATUS_AWAY)
+ awayUser2 := createUserWithStatus("away2", model.STATUS_AWAY)
+ dndUser1 := createUserWithStatus("dnd1", model.STATUS_DND)
+ dndUser2 := createUserWithStatus("dnd2", model.STATUS_DND)
+ offlineUser1 := createUserWithStatus("offline1", model.STATUS_OFFLINE)
+ offlineUser2 := createUserWithStatus("offline2", model.STATUS_OFFLINE)
+ onlineUser1 := createUserWithStatus("online1", model.STATUS_ONLINE)
+ onlineUser2 := createUserWithStatus("online2", model.STATUS_ONLINE)
+
+ t.Run("sorting by status then alphabetical", func(t *testing.T) {
+ usersByStatus, err := th.App.GetUsersInChannelPageByStatus(channel.Id, 0, 8, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectedUsersByStatus := []*model.User{
+ onlineUser1,
+ onlineUser2,
+ awayUser1,
+ awayUser2,
+ dndUser1,
+ dndUser2,
+ offlineUser1,
+ offlineUser2,
+ }
+
+ if len(usersByStatus) != len(expectedUsersByStatus) {
+ t.Fatalf("received only %v users, expected %v", len(usersByStatus), len(expectedUsersByStatus))
+ }
+
+ for i := range usersByStatus {
+ if usersByStatus[i].Id != expectedUsersByStatus[i].Id {
+ t.Fatalf("received user %v at index %v, expected %v", usersByStatus[i].Username, i, expectedUsersByStatus[i].Username)
+ }
+ }
+ })
+
+ t.Run("paging", func(t *testing.T) {
+ usersByStatus, err := th.App.GetUsersInChannelPageByStatus(channel.Id, 0, 3, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(usersByStatus) != 3 {
+ t.Fatal("received too many users")
+ }
+
+ if usersByStatus[0].Id != onlineUser1.Id && usersByStatus[1].Id != onlineUser2.Id {
+ t.Fatal("expected to receive online users first")
+ }
+
+ if usersByStatus[2].Id != awayUser1.Id {
+ t.Fatal("expected to receive away users second")
+ }
+
+ usersByStatus, err = th.App.GetUsersInChannelPageByStatus(channel.Id, 1, 3, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if usersByStatus[0].Id != awayUser2.Id {
+ t.Fatal("expected to receive away users second")
+ }
+
+ if usersByStatus[1].Id != dndUser1.Id && usersByStatus[2].Id != dndUser2.Id {
+ t.Fatal("expected to receive dnd users third")
+ }
+
+ usersByStatus, err = th.App.GetUsersInChannelPageByStatus(channel.Id, 1, 4, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(usersByStatus) != 4 {
+ t.Fatal("received too many users")
+ }
+
+ if usersByStatus[0].Id != dndUser1.Id && usersByStatus[1].Id != dndUser2.Id {
+ t.Fatal("expected to receive dnd users third")
+ }
+
+ if usersByStatus[2].Id != offlineUser1.Id && usersByStatus[3].Id != offlineUser2.Id {
+ t.Fatal("expected to receive offline users last")
+ }
+ })
+}
diff --git a/app/webhook.go b/app/webhook.go
index f3777ab48..abfc388b5 100644
--- a/app/webhook.go
+++ b/app/webhook.go
@@ -225,7 +225,7 @@ func SplitWebhookPost(post *model.Post) ([]*model.Post, *model.AppError) {
func (a *App) CreateWebhookPost(userId string, channel *model.Channel, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
// parse links into Markdown format
- linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
+ linkWithTextRegex := regexp.MustCompile(`<([^\n<\|>]+)\|([^\n>]+)>`)
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
post := &model.Post{UserId: userId, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId}
diff --git a/app/webhook_test.go b/app/webhook_test.go
index 850e74efc..4d2bc58fa 100644
--- a/app/webhook_test.go
+++ b/app/webhook_test.go
@@ -317,6 +317,64 @@ func TestCreateWebhookPost(t *testing.T) {
if err == nil {
t.Fatal("should have failed - bad post type")
}
+
+ expectedText := "`<>|<>|`"
+ post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{
+ "attachments": []*model.SlackAttachment{
+ {
+ Text: "text",
+ },
+ },
+ "webhook_display_name": hook.DisplayName,
+ }, model.POST_SLACK_ATTACHMENT, "")
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ assert.Equal(t, expectedText, post.Message)
+
+ expectedText = "< | \n|\n>"
+ post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{
+ "attachments": []*model.SlackAttachment{
+ {
+ Text: "text",
+ },
+ },
+ "webhook_display_name": hook.DisplayName,
+ }, model.POST_SLACK_ATTACHMENT, "")
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ assert.Equal(t, expectedText, post.Message)
+
+ expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a
+Author: foo <foo@example.org>
+Date: Thu Mar 1 19:46:54 2018 +0300
+
+ commit message 2
+
+ test | 1 +
+ 1 file changed, 1 insertion(+)
+
+commit 5df78b7139b543997838071cd912e375d8bd69b2
+Author: foo <foo@example.org>
+Date: Thu Mar 1 19:46:48 2018 +0300
+
+ commit message 1
+
+ test | 3 +++
+ 1 file changed, 3 insertions(+)`
+ post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{
+ "attachments": []*model.SlackAttachment{
+ {
+ Text: "text",
+ },
+ },
+ "webhook_display_name": hook.DisplayName,
+ }, model.POST_SLACK_ATTACHMENT, "")
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ assert.Equal(t, expectedText, post.Message)
}
func TestSplitWebhookPost(t *testing.T) {
diff --git a/build/Jenkinsfile b/build/Jenkinsfile
index ebef7ca3d..7eb2fda88 100644
--- a/build/Jenkinsfile
+++ b/build/Jenkinsfile
@@ -31,7 +31,7 @@ podTemplate(label: 'jenkins-slave',
),
containerTemplate(
name: 'mattermost-inbucket',
- image: 'jhillyerd/inbucket:latest',
+ image: 'jhillyerd/inbucket:release-1.2.0',
resourceRequestCpu: '250m',
resourceLimitCpu: '250m',
resourceRequestMemory: '256Mi',
diff --git a/build/release.mk b/build/release.mk
index 8bd4f9afd..568616ec3 100644
--- a/build/release.mk
+++ b/build/release.mk
@@ -3,15 +3,27 @@ dist: | check-style test package
build-linux:
@echo Build Linux amd64
- env GOOS=linux GOARCH=amd64 $(GO) install $(GOFLAGS) $(GO_LINKER_FLAGS) ./cmd/platform
+ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64")
+ env GOOS=linux GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./
+else
+ env GOOS=linux GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/linux_amd64/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./
+endif
build-osx:
@echo Build OSX amd64
- env GOOS=darwin GOARCH=amd64 $(GO) install $(GOFLAGS) $(GO_LINKER_FLAGS) ./cmd/platform
+ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64")
+ env GOOS=darwin GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./
+else
+ env GOOS=darwin GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/darwin_amd64/platform $(GOFLAGS) $(GO_LINKER_FLAGS) ./
+endif
build-windows:
@echo Build Windows amd64
- env GOOS=windows GOARCH=amd64 $(GO) install $(GOFLAGS) $(GO_LINKER_FLAGS) ./cmd/platform
+ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
+ env GOOS=windows GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/platform.exe $(GOFLAGS) $(GO_LINKER_FLAGS) ./
+else
+ env GOOS=windows GOARCH=amd64 $(GO) build -i -o $(GOPATH)/bin/windows_amd64/platform.exe $(GOFLAGS) $(GO_LINKER_FLAGS) ./
+endif
build: build-linux build-windows build-osx
diff --git a/cmd/cmd.go b/cmd/cmd.go
new file mode 100644
index 000000000..5a1a25bd9
--- /dev/null
+++ b/cmd/cmd.go
@@ -0,0 +1,26 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+)
+
+type Command = cobra.Command
+
+func Run(args []string) error {
+ RootCmd.SetArgs(args)
+ return RootCmd.Execute()
+}
+
+var RootCmd = &cobra.Command{
+ Use: "platform",
+ Short: "Open source, self-hosted Slack-alternative",
+ Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`,
+}
+
+func init() {
+ RootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.")
+ RootCmd.PersistentFlags().Bool("disableconfigwatch", false, "When set config.json will not be loaded from disk when the file is changed.")
+}
diff --git a/cmd/platform/platform_test.go b/cmd/cmdtestlib.go
index 792cabe38..db97b1a41 100644
--- a/cmd/platform/platform_test.go
+++ b/cmd/cmdtestlib.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package cmd
import (
"flag"
@@ -30,7 +30,7 @@ func execArgs(t *testing.T, args []string) []string {
return append(append(ret, "--", "--disableconfigwatch"), args...)
}
-func checkCommand(t *testing.T, args ...string) string {
+func CheckCommand(t *testing.T, args ...string) string {
path, err := os.Executable()
require.NoError(t, err)
output, err := exec.Command(path, execArgs(t, args)...).CombinedOutput()
@@ -38,16 +38,8 @@ func checkCommand(t *testing.T, args ...string) string {
return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(string(output)), "PASS"))
}
-func runCommand(t *testing.T, args ...string) error {
+func RunCommand(t *testing.T, args ...string) error {
path, err := os.Executable()
require.NoError(t, err)
return exec.Command(path, execArgs(t, args)...).Run()
}
-
-func TestExecCommand(t *testing.T) {
- if filter := flag.Lookup("test.run").Value.String(); filter != "ExecCommand" {
- t.Skip("use -run ExecCommand to execute a command via the test executable")
- }
- rootCmd.SetArgs(flag.Args())
- require.NoError(t, rootCmd.Execute())
-}
diff --git a/cmd/platform/channel.go b/cmd/commands/channel.go
index 5d86ad9da..597a22450 100644
--- a/cmd/platform/channel.go
+++ b/cmd/commands/channel.go
@@ -1,22 +1,24 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/spf13/cobra"
)
-var channelCmd = &cobra.Command{
+var ChannelCmd = &cobra.Command{
Use: "channel",
Short: "Management of channels",
}
-var channelCreateCmd = &cobra.Command{
+var ChannelCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a channel",
Long: `Create a channel.`,
@@ -25,7 +27,7 @@ var channelCreateCmd = &cobra.Command{
RunE: createChannelCmdF,
}
-var removeChannelUsersCmd = &cobra.Command{
+var RemoveChannelUsersCmd = &cobra.Command{
Use: "remove [channel] [users]",
Short: "Remove users from channel",
Long: "Remove some users from channel",
@@ -33,7 +35,7 @@ var removeChannelUsersCmd = &cobra.Command{
RunE: removeChannelUsersCmdF,
}
-var addChannelUsersCmd = &cobra.Command{
+var AddChannelUsersCmd = &cobra.Command{
Use: "add [channel] [users]",
Short: "Add users to channel",
Long: "Add some users to channel",
@@ -41,7 +43,7 @@ var addChannelUsersCmd = &cobra.Command{
RunE: addChannelUsersCmdF,
}
-var archiveChannelsCmd = &cobra.Command{
+var ArchiveChannelsCmd = &cobra.Command{
Use: "archive [channels]",
Short: "Archive channels",
Long: `Archive some channels.
@@ -51,7 +53,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe
RunE: archiveChannelsCmdF,
}
-var deleteChannelsCmd = &cobra.Command{
+var DeleteChannelsCmd = &cobra.Command{
Use: "delete [channels]",
Short: "Delete channels",
Long: `Permanently delete some channels.
@@ -61,7 +63,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe
RunE: deleteChannelsCmdF,
}
-var listChannelsCmd = &cobra.Command{
+var ListChannelsCmd = &cobra.Command{
Use: "list [teams]",
Short: "List all channels on specified teams.",
Long: `List all channels on specified teams.
@@ -70,7 +72,7 @@ Archived channels are appended with ' (archived)'.`,
RunE: listChannelsCmdF,
}
-var moveChannelsCmd = &cobra.Command{
+var MoveChannelsCmd = &cobra.Command{
Use: "move [team] [channels]",
Short: "Moves channels to the specified team",
Long: `Moves the provided channels to the specified team.
@@ -80,7 +82,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe
RunE: moveChannelsCmdF,
}
-var restoreChannelsCmd = &cobra.Command{
+var RestoreChannelsCmd = &cobra.Command{
Use: "restore [channels]",
Short: "Restore some channels",
Long: `Restore a previously deleted channel
@@ -89,7 +91,7 @@ Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channe
RunE: restoreChannelsCmdF,
}
-var modifyChannelCmd = &cobra.Command{
+var ModifyChannelCmd = &cobra.Command{
Use: "modify [channel]",
Short: "Modify a channel's public/private type",
Long: `Change the public/private type of a channel.
@@ -99,55 +101,57 @@ Channel can be specified by [team]:[channel]. ie. myteam:mychannel or by channel
}
func init() {
- channelCreateCmd.Flags().String("name", "", "Channel Name")
- channelCreateCmd.Flags().String("display_name", "", "Channel Display Name")
- channelCreateCmd.Flags().String("team", "", "Team name or ID")
- channelCreateCmd.Flags().String("header", "", "Channel header")
- channelCreateCmd.Flags().String("purpose", "", "Channel purpose")
- channelCreateCmd.Flags().Bool("private", false, "Create a private channel.")
-
- moveChannelsCmd.Flags().String("username", "", "Required. Username who is moving the channel.")
-
- deleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channels.")
-
- modifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel")
- modifyChannelCmd.Flags().Bool("public", false, "Convert the channel to a public channel")
- modifyChannelCmd.Flags().String("username", "", "Required. Username who changes the channel privacy.")
-
- channelCmd.AddCommand(
- channelCreateCmd,
- removeChannelUsersCmd,
- addChannelUsersCmd,
- archiveChannelsCmd,
- deleteChannelsCmd,
- listChannelsCmd,
- moveChannelsCmd,
- restoreChannelsCmd,
- modifyChannelCmd,
+ ChannelCreateCmd.Flags().String("name", "", "Channel Name")
+ ChannelCreateCmd.Flags().String("display_name", "", "Channel Display Name")
+ ChannelCreateCmd.Flags().String("team", "", "Team name or ID")
+ ChannelCreateCmd.Flags().String("header", "", "Channel header")
+ ChannelCreateCmd.Flags().String("purpose", "", "Channel purpose")
+ ChannelCreateCmd.Flags().Bool("private", false, "Create a private channel.")
+
+ MoveChannelsCmd.Flags().String("username", "", "Required. Username who is moving the channel.")
+
+ DeleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channels.")
+
+ ModifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel")
+ ModifyChannelCmd.Flags().Bool("public", false, "Convert the channel to a public channel")
+ ModifyChannelCmd.Flags().String("username", "", "Required. Username who changes the channel privacy.")
+
+ ChannelCmd.AddCommand(
+ ChannelCreateCmd,
+ RemoveChannelUsersCmd,
+ AddChannelUsersCmd,
+ ArchiveChannelsCmd,
+ DeleteChannelsCmd,
+ ListChannelsCmd,
+ MoveChannelsCmd,
+ RestoreChannelsCmd,
+ ModifyChannelCmd,
)
+
+ cmd.RootCmd.AddCommand(ChannelCmd)
}
-func createChannelCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func createChannelCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
- name, errn := cmd.Flags().GetString("name")
+ name, errn := command.Flags().GetString("name")
if errn != nil || name == "" {
return errors.New("Name is required")
}
- displayname, errdn := cmd.Flags().GetString("display_name")
+ displayname, errdn := command.Flags().GetString("display_name")
if errdn != nil || displayname == "" {
return errors.New("Display Name is required")
}
- teamArg, errteam := cmd.Flags().GetString("team")
+ teamArg, errteam := command.Flags().GetString("team")
if errteam != nil || teamArg == "" {
return errors.New("Team is required")
}
- header, _ := cmd.Flags().GetString("header")
- purpose, _ := cmd.Flags().GetString("purpose")
- useprivate, _ := cmd.Flags().GetBool("private")
+ header, _ := command.Flags().GetString("header")
+ purpose, _ := command.Flags().GetString("purpose")
+ useprivate, _ := command.Flags().GetBool("private")
channelType := model.CHANNEL_OPEN
if useprivate {
@@ -176,8 +180,8 @@ func createChannelCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func removeChannelUsersCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func removeChannelUsersCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -201,16 +205,16 @@ func removeChannelUsersCmdF(cmd *cobra.Command, args []string) error {
func removeUserFromChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) {
if user == nil {
- CommandPrintErrorln("Can't find user '" + userArg + "'")
+ cmd.CommandPrintErrorln("Can't find user '" + userArg + "'")
return
}
if err := a.RemoveUserFromChannel(user.Id, "", channel); err != nil {
- CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error())
+ cmd.CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error())
}
}
-func addChannelUsersCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func addChannelUsersCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -234,16 +238,16 @@ func addChannelUsersCmdF(cmd *cobra.Command, args []string) error {
func addUserToChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) {
if user == nil {
- CommandPrintErrorln("Can't find user '" + userArg + "'")
+ cmd.CommandPrintErrorln("Can't find user '" + userArg + "'")
return
}
if _, err := a.AddUserToChannel(user, channel); err != nil {
- CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error())
+ cmd.CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error())
}
}
-func archiveChannelsCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func archiveChannelsCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -255,19 +259,19 @@ func archiveChannelsCmdF(cmd *cobra.Command, args []string) error {
channels := getChannelsFromChannelArgs(a, args)
for i, channel := range channels {
if channel == nil {
- CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
continue
}
if result := <-a.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil {
- CommandPrintErrorln("Unable to archive channel '" + channel.Name + "' error: " + result.Err.Error())
+ cmd.CommandPrintErrorln("Unable to archive channel '" + channel.Name + "' error: " + result.Err.Error())
}
}
return nil
}
-func deleteChannelsCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func deleteChannelsCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -276,10 +280,10 @@ func deleteChannelsCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Enter at least one channel to delete.")
}
- confirmFlag, _ := cmd.Flags().GetBool("confirm")
+ confirmFlag, _ := command.Flags().GetBool("confirm")
if !confirmFlag {
var confirm string
- CommandPrettyPrintln("Are you sure you want to delete the channels specified? All data will be permanently deleted? (YES/NO): ")
+ cmd.CommandPrettyPrintln("Are you sure you want to delete the channels specified? All data will be permanently deleted? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
@@ -289,13 +293,13 @@ func deleteChannelsCmdF(cmd *cobra.Command, args []string) error {
channels := getChannelsFromChannelArgs(a, args)
for i, channel := range channels {
if channel == nil {
- CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
continue
}
if err := deleteChannel(a, channel); err != nil {
- CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + err.Error())
+ cmd.CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + err.Error())
} else {
- CommandPrettyPrintln("Deleted channel '" + channel.Name + "'")
+ cmd.CommandPrettyPrintln("Deleted channel '" + channel.Name + "'")
}
}
@@ -306,8 +310,8 @@ func deleteChannel(a *app.App, channel *model.Channel) *model.AppError {
return a.PermanentDeleteChannel(channel)
}
-func moveChannelsCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func moveChannelsCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -321,7 +325,7 @@ func moveChannelsCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Unable to find destination team '" + args[0] + "'")
}
- username, erru := cmd.Flags().GetString("username")
+ username, erru := command.Flags().GetString("username")
if erru != nil || username == "" {
return errors.New("Username is required")
}
@@ -330,14 +334,14 @@ func moveChannelsCmdF(cmd *cobra.Command, args []string) error {
channels := getChannelsFromChannelArgs(a, args[1:])
for i, channel := range channels {
if channel == nil {
- CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
continue
}
originTeamID := channel.TeamId
if err := moveChannel(a, team, channel, user); err != nil {
- CommandPrintErrorln("Unable to move channel '" + channel.Name + "' error: " + err.Error())
+ cmd.CommandPrintErrorln("Unable to move channel '" + channel.Name + "' error: " + err.Error())
} else {
- CommandPrettyPrintln("Moved channel '" + channel.Name + "' to " + team.Name + "(" + team.Id + ") from " + originTeamID + ".")
+ cmd.CommandPrettyPrintln("Moved channel '" + channel.Name + "' to " + team.Name + "(" + team.Id + ") from " + originTeamID + ".")
}
}
@@ -358,7 +362,7 @@ func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *mod
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if result := <-a.Srv.Store.Webhook().UpdateIncoming(webhook); result.Err != nil {
- CommandPrintErrorln("Failed to move incoming webhook '" + webhook.Id + "' to new team.")
+ cmd.CommandPrintErrorln("Failed to move incoming webhook '" + webhook.Id + "' to new team.")
}
}
}
@@ -371,7 +375,7 @@ func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *mod
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if result := <-a.Srv.Store.Webhook().UpdateOutgoing(webhook); result.Err != nil {
- CommandPrintErrorln("Failed to move outgoing webhook '" + webhook.Id + "' to new team.")
+ cmd.CommandPrintErrorln("Failed to move outgoing webhook '" + webhook.Id + "' to new team.")
}
}
}
@@ -380,8 +384,8 @@ func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *mod
return nil
}
-func listChannelsCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func listChannelsCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -393,19 +397,19 @@ func listChannelsCmdF(cmd *cobra.Command, args []string) error {
teams := getTeamsFromTeamArgs(a, args)
for i, team := range teams {
if team == nil {
- CommandPrintErrorln("Unable to find team '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to find team '" + args[i] + "'")
continue
}
if result := <-a.Srv.Store.Channel().GetAll(team.Id); result.Err != nil {
- CommandPrintErrorln("Unable to list channels for '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to list channels for '" + args[i] + "'")
} else {
channels := result.Data.([]*model.Channel)
for _, channel := range channels {
if channel.DeleteAt > 0 {
- CommandPrettyPrintln(channel.Name + " (archived)")
+ cmd.CommandPrettyPrintln(channel.Name + " (archived)")
} else {
- CommandPrettyPrintln(channel.Name)
+ cmd.CommandPrettyPrintln(channel.Name)
}
}
}
@@ -414,8 +418,8 @@ func listChannelsCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func restoreChannelsCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func restoreChannelsCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -427,19 +431,19 @@ func restoreChannelsCmdF(cmd *cobra.Command, args []string) error {
channels := getChannelsFromChannelArgs(a, args)
for i, channel := range channels {
if channel == nil {
- CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
continue
}
if result := <-a.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil {
- CommandPrintErrorln("Unable to restore channel '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to restore channel '" + args[i] + "'")
}
}
return nil
}
-func modifyChannelCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func modifyChannelCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -448,13 +452,13 @@ func modifyChannelCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Enter at one channel to modify.")
}
- username, erru := cmd.Flags().GetString("username")
+ username, erru := command.Flags().GetString("username")
if erru != nil || username == "" {
return errors.New("Username is required")
}
- public, _ := cmd.Flags().GetBool("public")
- private, _ := cmd.Flags().GetBool("private")
+ public, _ := command.Flags().GetBool("public")
+ private, _ := command.Flags().GetBool("private")
if public == private {
return errors.New("You must specify only one of --public or --private")
diff --git a/cmd/platform/channel_test.go b/cmd/commands/channel_test.go
index cf8603cf3..bd19b020a 100644
--- a/cmd/platform/channel_test.go
+++ b/cmd/commands/channel_test.go
@@ -1,13 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"strings"
"testing"
"github.com/mattermost/mattermost-server/api"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/stretchr/testify/require"
)
@@ -18,13 +19,13 @@ func TestJoinChannel(t *testing.T) {
channel := th.CreateChannel(th.BasicClient, th.BasicTeam)
- checkCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
+ cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
// Joining twice should succeed
- checkCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
+ cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
// should fail because channel does not exist
- require.Error(t, runCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name+"asdf", th.BasicUser2.Email))
+ require.Error(t, cmd.RunCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name+"asdf", th.BasicUser2.Email))
}
func TestRemoveChannel(t *testing.T) {
@@ -33,15 +34,15 @@ func TestRemoveChannel(t *testing.T) {
channel := th.CreateChannel(th.BasicClient, th.BasicTeam)
- checkCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
+ cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
// should fail because channel does not exist
- require.Error(t, runCommand(t, "channel", "remove", th.BasicTeam.Name+":doesnotexist", th.BasicUser2.Email))
+ require.Error(t, cmd.RunCommand(t, "channel", "remove", th.BasicTeam.Name+":doesnotexist", th.BasicUser2.Email))
- checkCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
+ cmd.CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
// Leaving twice should succeed
- checkCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
+ cmd.CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
}
func TestMoveChannel(t *testing.T) {
@@ -63,12 +64,12 @@ func TestMoveChannel(t *testing.T) {
origin := team1.Name + ":" + channel.Name
dest := team2.Name
- checkCommand(t, "channel", "add", origin, adminEmail)
+ cmd.CheckCommand(t, "channel", "add", origin, adminEmail)
// should fail with nill because errors are logged instead of returned when a channel does not exist
- require.Nil(t, runCommand(t, "channel", "move", dest, team1.Name+":doesnotexist", "--username", adminUsername))
+ require.Nil(t, cmd.RunCommand(t, "channel", "move", dest, team1.Name+":doesnotexist", "--username", adminUsername))
- checkCommand(t, "channel", "move", dest, origin, "--username", adminUsername)
+ cmd.CheckCommand(t, "channel", "move", dest, origin, "--username", adminUsername)
}
func TestListChannels(t *testing.T) {
@@ -78,7 +79,7 @@ func TestListChannels(t *testing.T) {
channel := th.CreateChannel(th.BasicClient, th.BasicTeam)
th.BasicClient.Must(th.BasicClient.DeleteChannel(channel.Id))
- output := checkCommand(t, "channel", "list", th.BasicTeam.Name)
+ output := cmd.CheckCommand(t, "channel", "list", th.BasicTeam.Name)
if !strings.Contains(string(output), "town-square") {
t.Fatal("should have channels")
@@ -96,10 +97,10 @@ func TestRestoreChannel(t *testing.T) {
channel := th.CreateChannel(th.BasicClient, th.BasicTeam)
th.BasicClient.Must(th.BasicClient.DeleteChannel(channel.Id))
- checkCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name)
+ cmd.CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name)
// restoring twice should succeed
- checkCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name)
+ cmd.CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name)
}
func TestCreateChannel(t *testing.T) {
@@ -109,8 +110,8 @@ func TestCreateChannel(t *testing.T) {
id := model.NewId()
name := "name" + id
- checkCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--name", name)
+ cmd.CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--name", name)
name = name + "-private"
- checkCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--private", "--name", name)
+ cmd.CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--private", "--name", name)
}
diff --git a/cmd/platform/channelargs.go b/cmd/commands/channelargs.go
index c12a9cc9a..680fed34b 100644
--- a/cmd/platform/channelargs.go
+++ b/cmd/commands/channelargs.go
@@ -1,6 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"fmt"
diff --git a/cmd/platform/command.go b/cmd/commands/command.go
index bbc5b47da..0202714b6 100644
--- a/cmd/platform/command.go
+++ b/cmd/commands/command.go
@@ -1,20 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"errors"
"github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/spf13/cobra"
)
-var commandCmd = &cobra.Command{
+var CommandCmd = &cobra.Command{
Use: "command",
Short: "Management of slash commands",
}
-var commandMoveCmd = &cobra.Command{
+var CommandMoveCmd = &cobra.Command{
Use: "move",
Short: "Move a slash command to a different team",
Long: `Move a slash command to a different team. Commands can be specified by [team]:[command-trigger-word]. ie. myteam:trigger or by command ID.`,
@@ -23,13 +25,14 @@ var commandMoveCmd = &cobra.Command{
}
func init() {
- commandCmd.AddCommand(
- commandMoveCmd,
+ CommandCmd.AddCommand(
+ CommandMoveCmd,
)
+ cmd.RootCmd.AddCommand(CommandCmd)
}
-func moveCommandCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func moveCommandCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -44,16 +47,16 @@ func moveCommandCmdF(cmd *cobra.Command, args []string) error {
}
commands := getCommandsFromCommandArgs(a, args[1:])
- CommandPrintErrorln(commands)
+ cmd.CommandPrintErrorln(commands)
for i, command := range commands {
if command == nil {
- CommandPrintErrorln("Unable to find command '" + args[i+1] + "'")
+ cmd.CommandPrintErrorln("Unable to find command '" + args[i+1] + "'")
continue
}
if err := moveCommand(a, team, command); err != nil {
- CommandPrintErrorln("Unable to move command '" + command.Trigger + "' error: " + err.Error())
+ cmd.CommandPrintErrorln("Unable to move command '" + command.Trigger + "' error: " + err.Error())
} else {
- CommandPrettyPrintln("Moved command '" + command.Trigger + "'")
+ cmd.CommandPrettyPrintln("Moved command '" + command.Trigger + "'")
}
}
diff --git a/cmd/platform/commandargs.go b/cmd/commands/commandargs.go
index 96e756815..702f01c9a 100644
--- a/cmd/platform/commandargs.go
+++ b/cmd/commands/commandargs.go
@@ -1,6 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"fmt"
diff --git a/cmd/platform/config.go b/cmd/commands/config.go
index cd4356529..ef3b0f75e 100644
--- a/cmd/platform/config.go
+++ b/cmd/commands/config.go
@@ -1,23 +1,25 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"encoding/json"
"errors"
"os"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/spf13/cobra"
)
-var configCmd = &cobra.Command{
+var ConfigCmd = &cobra.Command{
Use: "config",
Short: "Configuration",
}
-var validateConfigCmd = &cobra.Command{
+var ValidateConfigCmd = &cobra.Command{
Use: "validate",
Short: "Validate config file",
Long: "If the config file is valid, this command will output a success message and have a zero exit code. If it is invalid, this command will output an error and have a non-zero exit code.",
@@ -25,15 +27,16 @@ var validateConfigCmd = &cobra.Command{
}
func init() {
- configCmd.AddCommand(
- validateConfigCmd,
+ ConfigCmd.AddCommand(
+ ValidateConfigCmd,
)
+ cmd.RootCmd.AddCommand(ConfigCmd)
}
-func configValidateCmdF(cmd *cobra.Command, args []string) error {
+func configValidateCmdF(command *cobra.Command, args []string) error {
utils.TranslationsPreInit()
model.AppErrorInit(utils.T)
- filePath, err := cmd.Flags().GetString("config")
+ filePath, err := command.Flags().GetString("config")
if err != nil {
return err
}
@@ -60,6 +63,6 @@ func configValidateCmdF(cmd *cobra.Command, args []string) error {
return errors.New(utils.T(err.Id))
}
- CommandPrettyPrintln("The document is valid")
+ cmd.CommandPrettyPrintln("The document is valid")
return nil
}
diff --git a/cmd/platform/mattermost_test.go b/cmd/commands/config_flag_test.go
index 7246d620f..8d284ab73 100644
--- a/cmd/platform/mattermost_test.go
+++ b/cmd/commands/config_flag_test.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"io/ioutil"
@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/utils"
)
@@ -34,8 +35,8 @@ func TestConfigFlag(t *testing.T) {
defer os.Chdir(prevDir)
os.Chdir(dir)
- require.Error(t, runCommand(t, "version"))
- checkCommand(t, "--config", "foo.json", "version")
- checkCommand(t, "--config", "./foo.json", "version")
- checkCommand(t, "--config", configPath, "version")
+ require.Error(t, cmd.RunCommand(t, "version"))
+ cmd.CheckCommand(t, "--config", "foo.json", "version")
+ cmd.CheckCommand(t, "--config", "./foo.json", "version")
+ cmd.CheckCommand(t, "--config", configPath, "version")
}
diff --git a/cmd/platform/config_test.go b/cmd/commands/config_test.go
index f1c09c6f5..54ddfcb61 100644
--- a/cmd/platform/config_test.go
+++ b/cmd/commands/config_test.go
@@ -1,7 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"io/ioutil"
@@ -9,6 +9,7 @@ import (
"path/filepath"
"testing"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -25,6 +26,6 @@ func TestConfigValidate(t *testing.T) {
config.SetDefaults()
require.NoError(t, ioutil.WriteFile(path, []byte(config.ToJson()), 0600))
- assert.Error(t, runCommand(t, "--config", "foo.json", "config", "validate"))
- assert.NoError(t, runCommand(t, "--config", path, "config", "validate"))
+ assert.Error(t, cmd.RunCommand(t, "--config", "foo.json", "config", "validate"))
+ assert.NoError(t, cmd.RunCommand(t, "--config", path, "config", "validate"))
}
diff --git a/cmd/commands/exec_command_test.go b/cmd/commands/exec_command_test.go
new file mode 100644
index 000000000..79e65fe83
--- /dev/null
+++ b/cmd/commands/exec_command_test.go
@@ -0,0 +1,21 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package commands
+
+import (
+ "flag"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/mattermost/mattermost-server/cmd"
+)
+
+func TestExecCommand(t *testing.T) {
+ if filter := flag.Lookup("test.run").Value.String(); filter != "ExecCommand" {
+ t.Skip("use -run ExecCommand to execute a command via the test executable")
+ }
+ cmd.RootCmd.SetArgs(flag.Args())
+ require.NoError(t, cmd.RootCmd.Execute())
+}
diff --git a/cmd/platform/import.go b/cmd/commands/import.go
index 44ada904f..4058d175a 100644
--- a/cmd/platform/import.go
+++ b/cmd/commands/import.go
@@ -1,6 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"errors"
@@ -8,15 +9,16 @@ import (
"fmt"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/spf13/cobra"
)
-var importCmd = &cobra.Command{
+var ImportCmd = &cobra.Command{
Use: "import",
Short: "Import data.",
}
-var slackImportCmd = &cobra.Command{
+var SlackImportCmd = &cobra.Command{
Use: "slack [team] [file]",
Short: "Import a team from Slack.",
Long: "Import a team from a Slack export zip file.",
@@ -24,7 +26,7 @@ var slackImportCmd = &cobra.Command{
RunE: slackImportCmdF,
}
-var bulkImportCmd = &cobra.Command{
+var BulkImportCmd = &cobra.Command{
Use: "bulk [file]",
Short: "Import bulk data.",
Long: "Import data from a Mattermost Bulk Import File.",
@@ -33,18 +35,19 @@ var bulkImportCmd = &cobra.Command{
}
func init() {
- bulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.")
- bulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.")
- bulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.")
+ BulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.")
+ BulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.")
+ BulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.")
- importCmd.AddCommand(
- bulkImportCmd,
- slackImportCmd,
+ ImportCmd.AddCommand(
+ BulkImportCmd,
+ SlackImportCmd,
)
+ cmd.RootCmd.AddCommand(ImportCmd)
}
-func slackImportCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func slackImportCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -69,32 +72,32 @@ func slackImportCmdF(cmd *cobra.Command, args []string) error {
return err
}
- CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.")
+ cmd.CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.")
a.SlackImport(fileReader, fileInfo.Size(), team.Id)
- CommandPrettyPrintln("Finished Slack Import.")
+ cmd.CommandPrettyPrintln("Finished Slack Import.")
return nil
}
-func bulkImportCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func bulkImportCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
- apply, err := cmd.Flags().GetBool("apply")
+ apply, err := command.Flags().GetBool("apply")
if err != nil {
return errors.New("Apply flag error")
}
- validate, err := cmd.Flags().GetBool("validate")
+ validate, err := command.Flags().GetBool("validate")
if err != nil {
return errors.New("Validate flag error")
}
- workers, err := cmd.Flags().GetInt("workers")
+ workers, err := command.Flags().GetInt("workers")
if err != nil {
return errors.New("Workers flag error")
}
@@ -110,28 +113,28 @@ func bulkImportCmdF(cmd *cobra.Command, args []string) error {
defer fileReader.Close()
if apply && validate {
- CommandPrettyPrintln("Use only one of --apply or --validate.")
+ cmd.CommandPrettyPrintln("Use only one of --apply or --validate.")
return nil
} else if apply && !validate {
- CommandPrettyPrintln("Running Bulk Import. This may take a long time.")
+ cmd.CommandPrettyPrintln("Running Bulk Import. This may take a long time.")
} else {
- CommandPrettyPrintln("Running Bulk Import Data Validation.")
- CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **")
- CommandPrettyPrintln("Use the --apply flag to perform the actual data import.")
+ cmd.CommandPrettyPrintln("Running Bulk Import Data Validation.")
+ cmd.CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **")
+ cmd.CommandPrettyPrintln("Use the --apply flag to perform the actual data import.")
}
- CommandPrettyPrintln("")
+ cmd.CommandPrettyPrintln("")
if err, lineNumber := a.BulkImport(fileReader, !apply, workers); err != nil {
- CommandPrettyPrintln(err.Error())
+ cmd.CommandPrettyPrintln(err.Error())
if lineNumber != 0 {
- CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))
+ cmd.CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))
}
} else {
if apply {
- CommandPrettyPrintln("Finished Bulk Import.")
+ cmd.CommandPrettyPrintln("Finished Bulk Import.")
} else {
- CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.")
+ cmd.CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.")
}
}
diff --git a/cmd/platform/jobserver.go b/cmd/commands/jobserver.go
index 044ee6b6a..b96984b41 100644
--- a/cmd/platform/jobserver.go
+++ b/cmd/commands/jobserver.go
@@ -1,6 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"os"
@@ -8,27 +9,30 @@ import (
"syscall"
l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/spf13/cobra"
)
-var jobserverCmd = &cobra.Command{
+var JobserverCmd = &cobra.Command{
Use: "jobserver",
Short: "Start the Mattermost job server",
Run: jobserverCmdF,
}
func init() {
- jobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.")
- jobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.")
+ JobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.")
+ JobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.")
+
+ cmd.RootCmd.AddCommand(JobserverCmd)
}
-func jobserverCmdF(cmd *cobra.Command, args []string) {
+func jobserverCmdF(command *cobra.Command, args []string) {
// Options
- noJobs, _ := cmd.Flags().GetBool("nojobs")
- noSchedule, _ := cmd.Flags().GetBool("noschedule")
+ noJobs, _ := command.Flags().GetBool("nojobs")
+ noSchedule, _ := command.Flags().GetBool("noschedule")
// Initialize
- a, err := initDBCommandContext("config.json")
+ a, err := cmd.InitDBCommandContext("config.json")
if err != nil {
panic(err.Error())
}
diff --git a/cmd/platform/ldap.go b/cmd/commands/ldap.go
index 1bbcaa2f5..6938eae28 100644
--- a/cmd/platform/ldap.go
+++ b/cmd/commands/ldap.go
@@ -1,18 +1,20 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/spf13/cobra"
)
-var ldapCmd = &cobra.Command{
+var LdapCmd = &cobra.Command{
Use: "ldap",
Short: "LDAP related utilities",
}
-var ldapSyncCmd = &cobra.Command{
+var LdapSyncCmd = &cobra.Command{
Use: "sync",
Short: "Synchronize now",
Long: "Synchronize all LDAP users now.",
@@ -21,13 +23,14 @@ var ldapSyncCmd = &cobra.Command{
}
func init() {
- ldapCmd.AddCommand(
- ldapSyncCmd,
+ LdapCmd.AddCommand(
+ LdapSyncCmd,
)
+ cmd.RootCmd.AddCommand(LdapCmd)
}
-func ldapSyncCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func ldapSyncCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -35,9 +38,9 @@ func ldapSyncCmdF(cmd *cobra.Command, args []string) error {
if ldapI := a.Ldap; ldapI != nil {
job, err := ldapI.StartSynchronizeJob(true)
if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED {
- CommandPrintErrorln("ERROR: AD/LDAP Synchronization please check the server logs")
+ cmd.CommandPrintErrorln("ERROR: AD/LDAP Synchronization please check the server logs")
} else {
- CommandPrettyPrintln("SUCCESS: AD/LDAP Synchronization Complete")
+ cmd.CommandPrettyPrintln("SUCCESS: AD/LDAP Synchronization Complete")
}
}
diff --git a/cmd/platform/license.go b/cmd/commands/license.go
index 73efe9137..dce257a5d 100644
--- a/cmd/platform/license.go
+++ b/cmd/commands/license.go
@@ -1,20 +1,22 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"errors"
"io/ioutil"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/spf13/cobra"
)
-var licenseCmd = &cobra.Command{
+var LicenseCmd = &cobra.Command{
Use: "license",
Short: "Licensing commands",
}
-var uploadLicenseCmd = &cobra.Command{
+var UploadLicenseCmd = &cobra.Command{
Use: "upload [license]",
Short: "Upload a license.",
Long: "Upload a license. Replaces current license.",
@@ -23,11 +25,12 @@ var uploadLicenseCmd = &cobra.Command{
}
func init() {
- licenseCmd.AddCommand(uploadLicenseCmd)
+ LicenseCmd.AddCommand(UploadLicenseCmd)
+ cmd.RootCmd.AddCommand(LicenseCmd)
}
-func uploadLicenseCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func uploadLicenseCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -45,7 +48,7 @@ func uploadLicenseCmdF(cmd *cobra.Command, args []string) error {
return err
}
- CommandPrettyPrintln("Uploaded license file")
+ cmd.CommandPrettyPrintln("Uploaded license file")
return nil
}
diff --git a/cmd/platform/message_export.go b/cmd/commands/message_export.go
index fb1f4073b..7162d46c2 100644
--- a/cmd/platform/message_export.go
+++ b/cmd/commands/message_export.go
@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"errors"
@@ -10,11 +10,12 @@ import (
"time"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/spf13/cobra"
)
-var messageExportCmd = &cobra.Command{
+var MessageExportCmd = &cobra.Command{
Use: "export",
Short: "Export data from Mattermost",
Long: "Export data from Mattermost in a format suitable for import into a third-party application",
@@ -23,13 +24,14 @@ var messageExportCmd = &cobra.Command{
}
func init() {
- messageExportCmd.Flags().String("format", "actiance", "The format to export data in")
- messageExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
- messageExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.")
+ MessageExportCmd.Flags().String("format", "actiance", "The format to export data in")
+ MessageExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
+ MessageExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.")
+ cmd.RootCmd.AddCommand(MessageExportCmd)
}
-func messageExportCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func messageExportCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -39,20 +41,20 @@ func messageExportCmdF(cmd *cobra.Command, args []string) error {
}
// for now, format is hard-coded to actiance. In time, we'll have to support other formats and inject them into job data
- if format, err := cmd.Flags().GetString("format"); err != nil {
+ if format, err := command.Flags().GetString("format"); err != nil {
return errors.New("format flag error")
} else if format != "actiance" {
return errors.New("unsupported export format")
}
- startTime, err := cmd.Flags().GetInt64("exportFrom")
+ startTime, err := command.Flags().GetInt64("exportFrom")
if err != nil {
return errors.New("exportFrom flag error")
} else if startTime < 0 {
return errors.New("exportFrom must be a positive integer")
}
- timeoutSeconds, err := cmd.Flags().GetInt("timeoutSeconds")
+ timeoutSeconds, err := command.Flags().GetInt("timeoutSeconds")
if err != nil {
return errors.New("timeoutSeconds error")
} else if timeoutSeconds < 0 {
@@ -69,9 +71,9 @@ func messageExportCmdF(cmd *cobra.Command, args []string) error {
job, err := messageExportI.StartSynchronizeJob(ctx, startTime)
if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED {
- CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs")
+ cmd.CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs")
} else {
- CommandPrettyPrintln("SUCCESS: Message export job complete")
+ cmd.CommandPrettyPrintln("SUCCESS: Message export job complete")
}
}
diff --git a/cmd/platform/message_export_test.go b/cmd/commands/message_export_test.go
index 386aa4268..5170b77af 100644
--- a/cmd/platform/message_export_test.go
+++ b/cmd/commands/message_export_test.go
@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"io/ioutil"
@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
)
@@ -24,7 +25,7 @@ func TestMessageExportNotEnabled(t *testing.T) {
defer os.RemoveAll(filepath.Dir(configPath))
// should fail fast because the feature isn't enabled
- require.Error(t, runCommand(t, "--config", configPath, "export"))
+ require.Error(t, cmd.RunCommand(t, "--config", configPath, "export"))
}
func TestMessageExportInvalidFormat(t *testing.T) {
@@ -32,7 +33,7 @@ func TestMessageExportInvalidFormat(t *testing.T) {
defer os.RemoveAll(filepath.Dir(configPath))
// should fail fast because format isn't supported
- require.Error(t, runCommand(t, "--config", configPath, "--format", "not_actiance", "export"))
+ require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "not_actiance", "export"))
}
func TestMessageExportNegativeExportFrom(t *testing.T) {
@@ -40,7 +41,7 @@ func TestMessageExportNegativeExportFrom(t *testing.T) {
defer os.RemoveAll(filepath.Dir(configPath))
// should fail fast because export from must be a valid timestamp
- require.Error(t, runCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "-1", "export"))
+ require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "-1", "export"))
}
func TestMessageExportNegativeTimeoutSeconds(t *testing.T) {
@@ -48,7 +49,7 @@ func TestMessageExportNegativeTimeoutSeconds(t *testing.T) {
defer os.RemoveAll(filepath.Dir(configPath))
// should fail fast because timeout seconds must be a positive int
- require.Error(t, runCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "0", "--timeoutSeconds", "-1", "export"))
+ require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "0", "--timeoutSeconds", "-1", "export"))
}
func writeTempConfig(t *testing.T, isMessageExportEnabled bool) string {
diff --git a/cmd/commands/reset.go b/cmd/commands/reset.go
new file mode 100644
index 000000000..e479d0354
--- /dev/null
+++ b/cmd/commands/reset.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package commands
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/mattermost/mattermost-server/cmd"
+ "github.com/spf13/cobra"
+)
+
+var ResetCmd = &cobra.Command{
+ Use: "reset",
+ Short: "Reset the database to initial state",
+ Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.",
+ RunE: resetCmdF,
+}
+
+func init() {
+ ResetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.")
+
+ cmd.RootCmd.AddCommand(ResetCmd)
+}
+
+func resetCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
+ if err != nil {
+ return err
+ }
+
+ confirmFlag, _ := command.Flags().GetBool("confirm")
+ if !confirmFlag {
+ var confirm string
+ cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
+ fmt.Scanln(&confirm)
+
+ if confirm != "YES" {
+ return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
+ }
+ cmd.CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ")
+ fmt.Scanln(&confirm)
+ if confirm != "YES" {
+ return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
+ }
+ }
+
+ a.Srv.Store.DropAllTables()
+ cmd.CommandPrettyPrintln("Database sucessfully reset")
+
+ return nil
+}
diff --git a/cmd/platform/roles.go b/cmd/commands/roles.go
index e7a1c1a0e..bf7c39476 100644
--- a/cmd/platform/roles.go
+++ b/cmd/commands/roles.go
@@ -1,19 +1,21 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"errors"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/spf13/cobra"
)
-var rolesCmd = &cobra.Command{
+var RolesCmd = &cobra.Command{
Use: "roles",
Short: "Management of user roles",
}
-var makeSystemAdminCmd = &cobra.Command{
+var MakeSystemAdminCmd = &cobra.Command{
Use: "system_admin [users]",
Short: "Set a user as system admin",
Long: "Make some users system admins",
@@ -21,7 +23,7 @@ var makeSystemAdminCmd = &cobra.Command{
RunE: makeSystemAdminCmdF,
}
-var makeMemberCmd = &cobra.Command{
+var MakeMemberCmd = &cobra.Command{
Use: "member [users]",
Short: "Remove system admin privileges",
Long: "Remove system admin privileges from some users.",
@@ -30,14 +32,15 @@ var makeMemberCmd = &cobra.Command{
}
func init() {
- rolesCmd.AddCommand(
- makeSystemAdminCmd,
- makeMemberCmd,
+ RolesCmd.AddCommand(
+ MakeSystemAdminCmd,
+ MakeMemberCmd,
)
+ cmd.RootCmd.AddCommand(RolesCmd)
}
-func makeSystemAdminCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func makeSystemAdminCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -60,8 +63,8 @@ func makeSystemAdminCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func makeMemberCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func makeMemberCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
diff --git a/cmd/platform/roles_test.go b/cmd/commands/roles_test.go
index 1a5ae5173..1e0a46a4e 100644
--- a/cmd/platform/roles_test.go
+++ b/cmd/commands/roles_test.go
@@ -1,12 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"testing"
"github.com/mattermost/mattermost-server/api"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
)
@@ -14,7 +15,7 @@ func TestAssignRole(t *testing.T) {
th := api.Setup().InitBasic()
defer th.TearDown()
- checkCommand(t, "roles", "system_admin", th.BasicUser.Email)
+ cmd.CheckCommand(t, "roles", "system_admin", th.BasicUser.Email)
if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil {
t.Fatal()
diff --git a/cmd/platform/sampledata.go b/cmd/commands/sampledata.go
index 98a041d8f..5377f1153 100644
--- a/cmd/platform/sampledata.go
+++ b/cmd/commands/sampledata.go
@@ -1,6 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"encoding/json"
@@ -16,15 +17,34 @@ import (
"github.com/icrowley/fake"
"github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/spf13/cobra"
)
-var sampleDataCmd = &cobra.Command{
+var SampleDataCmd = &cobra.Command{
Use: "sampledata",
Short: "Generate sample data",
RunE: sampleDataCmdF,
}
+func init() {
+ SampleDataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).")
+ SampleDataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.")
+ SampleDataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.")
+ SampleDataCmd.Flags().IntP("users", "u", 15, "The number of sample users.")
+ SampleDataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.")
+ SampleDataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.")
+ SampleDataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.")
+ SampleDataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.")
+ SampleDataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.")
+ SampleDataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.")
+ SampleDataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.")
+ SampleDataCmd.Flags().IntP("workers", "w", 2, "How many workers to run during the import.")
+ SampleDataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.")
+ SampleDataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of loading into the database.")
+ cmd.RootCmd.AddCommand(SampleDataCmd)
+}
+
func sliceIncludes(vs []string, t string) bool {
for _, v := range vs {
if v == t {
@@ -109,81 +129,64 @@ func randomMessage(users []string) string {
return message
}
-func init() {
- sampleDataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).")
- sampleDataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.")
- sampleDataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.")
- sampleDataCmd.Flags().IntP("users", "u", 15, "The number of sample users.")
- sampleDataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.")
- sampleDataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.")
- sampleDataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.")
- sampleDataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.")
- sampleDataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.")
- sampleDataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.")
- sampleDataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.")
- sampleDataCmd.Flags().IntP("workers", "w", 2, "How many workers to run during the import.")
- sampleDataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.")
- sampleDataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of loading into the database.")
-}
-
-func sampleDataCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func sampleDataCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
- seed, err := cmd.Flags().GetInt64("seed")
+ seed, err := command.Flags().GetInt64("seed")
if err != nil {
return errors.New("Invalid seed parameter")
}
- bulk, err := cmd.Flags().GetString("bulk")
+ bulk, err := command.Flags().GetString("bulk")
if err != nil {
return errors.New("Invalid bulk parameter")
}
- teams, err := cmd.Flags().GetInt("teams")
+ teams, err := command.Flags().GetInt("teams")
if err != nil || teams < 0 {
return errors.New("Invalid teams parameter")
}
- channelsPerTeam, err := cmd.Flags().GetInt("channels-per-team")
+ channelsPerTeam, err := command.Flags().GetInt("channels-per-team")
if err != nil || channelsPerTeam < 0 {
return errors.New("Invalid channels-per-team parameter")
}
- users, err := cmd.Flags().GetInt("users")
+ users, err := command.Flags().GetInt("users")
if err != nil || users < 0 {
return errors.New("Invalid users parameter")
}
- teamMemberships, err := cmd.Flags().GetInt("team-memberships")
+ teamMemberships, err := command.Flags().GetInt("team-memberships")
if err != nil || teamMemberships < 0 {
return errors.New("Invalid team-memberships parameter")
}
- channelMemberships, err := cmd.Flags().GetInt("channel-memberships")
+ channelMemberships, err := command.Flags().GetInt("channel-memberships")
if err != nil || channelMemberships < 0 {
return errors.New("Invalid channel-memberships parameter")
}
- postsPerChannel, err := cmd.Flags().GetInt("posts-per-channel")
+ postsPerChannel, err := command.Flags().GetInt("posts-per-channel")
if err != nil || postsPerChannel < 0 {
return errors.New("Invalid posts-per-channel parameter")
}
- directChannels, err := cmd.Flags().GetInt("direct-channels")
+ directChannels, err := command.Flags().GetInt("direct-channels")
if err != nil || directChannels < 0 {
return errors.New("Invalid direct-channels parameter")
}
- postsPerDirectChannel, err := cmd.Flags().GetInt("posts-per-direct-channel")
+ postsPerDirectChannel, err := command.Flags().GetInt("posts-per-direct-channel")
if err != nil || postsPerDirectChannel < 0 {
return errors.New("Invalid posts-per-direct-channel parameter")
}
- groupChannels, err := cmd.Flags().GetInt("group-channels")
+ groupChannels, err := command.Flags().GetInt("group-channels")
if err != nil || groupChannels < 0 {
return errors.New("Invalid group-channels parameter")
}
- postsPerGroupChannel, err := cmd.Flags().GetInt("posts-per-group-channel")
+ postsPerGroupChannel, err := command.Flags().GetInt("posts-per-group-channel")
if err != nil || postsPerGroupChannel < 0 {
return errors.New("Invalid posts-per-group-channel parameter")
}
- workers, err := cmd.Flags().GetInt("workers")
+ workers, err := command.Flags().GetInt("workers")
if err != nil {
return errors.New("Invalid workers parameter")
}
- profileImagesPath, err := cmd.Flags().GetString("profile-images")
+ profileImagesPath, err := command.Flags().GetString("profile-images")
if err != nil {
return errors.New("Invalid profile-images parameter")
}
@@ -312,7 +315,7 @@ func sampleDataCmdF(cmd *cobra.Command, args []string) error {
}
importErr, lineNumber := a.BulkImport(bulkFile, false, workers)
if importErr != nil {
- return errors.New(fmt.Sprintf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber))
+ return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber)
}
} else if bulk != "-" {
err := bulkFile.Close()
@@ -395,7 +398,7 @@ func createUser(idx int, teamMemberships int, channelMemberships int, teamsAndCh
position := rand.Intn(len(possibleTeams))
team := possibleTeams[position]
possibleTeams = append(possibleTeams[:position], possibleTeams[position+1:]...)
- if teamChannels, err := teamsAndChannels[team]; err == true {
+ if teamChannels, err := teamsAndChannels[team]; err {
teams = append(teams, createTeamMembership(channelMemberships, teamChannels, &team))
}
}
@@ -429,10 +432,7 @@ func createTeamMembership(numOfchannels int, teamChannels []string, teamName *st
roles = "team_user team_admin"
}
channels := []app.UserChannelImportData{}
- teamChannelsCopy := []string{}
- for _, value := range teamChannels {
- teamChannelsCopy = append(teamChannelsCopy, value)
- }
+ teamChannelsCopy := append([]string(nil), teamChannels...)
for x := 0; x < numOfchannels; x++ {
if len(teamChannelsCopy) == 0 {
break
diff --git a/cmd/platform/sampledata_test.go b/cmd/commands/sampledata_test.go
index de28c0856..d71ac0575 100644
--- a/cmd/platform/sampledata_test.go
+++ b/cmd/commands/sampledata_test.go
@@ -1,12 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"testing"
"github.com/mattermost/mattermost-server/api"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/stretchr/testify/require"
)
@@ -15,11 +16,11 @@ func TestSampledataBadParameters(t *testing.T) {
defer th.TearDown()
// should fail because you need at least 1 worker
- require.Error(t, runCommand(t, "sampledata", "--workers", "0"))
+ require.Error(t, cmd.RunCommand(t, "sampledata", "--workers", "0"))
// should fail because you have more team memberships than teams
- require.Error(t, runCommand(t, "sampledata", "--teams", "10", "--teams-memberships", "11"))
+ require.Error(t, cmd.RunCommand(t, "sampledata", "--teams", "10", "--teams-memberships", "11"))
// should fail because you have more channel memberships than channels per team
- require.Error(t, runCommand(t, "sampledata", "--channels-per-team", "10", "--channel-memberships", "11"))
+ require.Error(t, cmd.RunCommand(t, "sampledata", "--channels-per-team", "10", "--channel-memberships", "11"))
}
diff --git a/cmd/platform/server.go b/cmd/commands/server.go
index 1ea38455c..9048696ca 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..a8b7341b2 100644
--- a/cmd/platform/user.go
+++ b/cmd/commands/user.go
@@ -1,6 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"encoding/json"
@@ -10,16 +11,17 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/spf13/cobra"
)
-var userCmd = &cobra.Command{
+var UserCmd = &cobra.Command{
Use: "user",
Short: "Management of users",
}
-var userActivateCmd = &cobra.Command{
+var UserActivateCmd = &cobra.Command{
Use: "activate [emails, usernames, userIds]",
Short: "Activate users",
Long: "Activate users that have been deactivated.",
@@ -28,7 +30,7 @@ var userActivateCmd = &cobra.Command{
RunE: userActivateCmdF,
}
-var userDeactivateCmd = &cobra.Command{
+var UserDeactivateCmd = &cobra.Command{
Use: "deactivate [emails, usernames, userIds]",
Short: "Deactivate users",
Long: "Deactivate users. Deactivated users are immediately logged out of all sessions and are unable to log back in.",
@@ -37,7 +39,7 @@ var userDeactivateCmd = &cobra.Command{
RunE: userDeactivateCmdF,
}
-var userCreateCmd = &cobra.Command{
+var UserCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a user",
Long: "Create a user",
@@ -45,7 +47,7 @@ var userCreateCmd = &cobra.Command{
RunE: userCreateCmdF,
}
-var userInviteCmd = &cobra.Command{
+var UserInviteCmd = &cobra.Command{
Use: "invite [email] [teams]",
Short: "Send user an email invite to a team.",
Long: `Send user an email invite to a team.
@@ -56,7 +58,7 @@ You can specify teams by name or ID.`,
RunE: userInviteCmdF,
}
-var resetUserPasswordCmd = &cobra.Command{
+var ResetUserPasswordCmd = &cobra.Command{
Use: "password [user] [password]",
Short: "Set a user's password",
Long: "Set a user's password",
@@ -64,7 +66,16 @@ var resetUserPasswordCmd = &cobra.Command{
RunE: resetUserPasswordCmdF,
}
-var resetUserMfaCmd = &cobra.Command{
+var updateUserEmailCmd = &cobra.Command{
+ Use: "email [user] [new email]",
+ Short: "Change email of the user",
+ Long: "Change email of the user.",
+ Example: ` user email test user@example.com
+ user activate username`,
+ RunE: updateUserEmailCmdF,
+}
+
+var ResetUserMfaCmd = &cobra.Command{
Use: "resetmfa [users]",
Short: "Turn off MFA",
Long: `Turn off multi-factor authentication for a user.
@@ -73,7 +84,7 @@ If MFA enforcement is enabled, the user will be forced to re-enable MFA as soon
RunE: resetUserMfaCmdF,
}
-var deleteUserCmd = &cobra.Command{
+var DeleteUserCmd = &cobra.Command{
Use: "delete [users]",
Short: "Delete users and all posts",
Long: "Permanently delete user and all related information including posts.",
@@ -81,7 +92,7 @@ var deleteUserCmd = &cobra.Command{
RunE: deleteUserCmdF,
}
-var deleteAllUsersCmd = &cobra.Command{
+var DeleteAllUsersCmd = &cobra.Command{
Use: "deleteall",
Short: "Delete all users and all posts",
Long: "Permanently delete all users and all related information including posts.",
@@ -89,7 +100,7 @@ var deleteAllUsersCmd = &cobra.Command{
RunE: deleteAllUsersCommandF,
}
-var migrateAuthCmd = &cobra.Command{
+var MigrateAuthCmd = &cobra.Command{
Use: "migrate_auth [from_auth] [to_auth] [migration-options]",
Short: "Mass migrate user accounts authentication type",
Long: `Migrates accounts from one authentication provider to another. For example, you can upgrade your authentication provider from email to ldap.`,
@@ -127,7 +138,7 @@ var migrateAuthCmd = &cobra.Command{
RunE: migrateAuthCmdF,
}
-var verifyUserCmd = &cobra.Command{
+var VerifyUserCmd = &cobra.Command{
Use: "verify [users]",
Short: "Verify email of users",
Long: "Verify the emails of some users.",
@@ -135,7 +146,7 @@ var verifyUserCmd = &cobra.Command{
RunE: verifyUserCmdF,
}
-var searchUserCmd = &cobra.Command{
+var SearchUserCmd = &cobra.Command{
Use: "search [users]",
Short: "Search for users",
Long: "Search for users based on username, email, or user ID.",
@@ -144,23 +155,23 @@ var searchUserCmd = &cobra.Command{
}
func init() {
- userCreateCmd.Flags().String("username", "", "Required. Username for the new user account.")
- userCreateCmd.Flags().String("email", "", "Required. The email address for the new user account.")
- userCreateCmd.Flags().String("password", "", "Required. The password for the new user account.")
- userCreateCmd.Flags().String("nickname", "", "Optional. The nickname for the new user account.")
- userCreateCmd.Flags().String("firstname", "", "Optional. The first name for the new user account.")
- userCreateCmd.Flags().String("lastname", "", "Optional. The last name for the new user account.")
- userCreateCmd.Flags().String("locale", "", "Optional. The locale (ex: en, fr) for the new user account.")
- userCreateCmd.Flags().Bool("system_admin", false, "Optional. If supplied, the new user will be a system administrator. Defaults to false.")
-
- deleteUserCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.")
-
- deleteAllUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.")
-
- migrateAuthCmd.Flags().Bool("force", false, "Force the migration to occur even if there are duplicates on the LDAP server. Duplicates will not be migrated. (ldap only)")
- migrateAuthCmd.Flags().Bool("auto", false, "Automatically migrate all users. Assumes the usernames and emails are identical between Mattermost and SAML services. (saml only)")
- migrateAuthCmd.Flags().Bool("dryRun", false, "Run a simulation of the migration process without changing the database.")
- migrateAuthCmd.SetUsageTemplate(`Usage:
+ UserCreateCmd.Flags().String("username", "", "Required. Username for the new user account.")
+ UserCreateCmd.Flags().String("email", "", "Required. The email address for the new user account.")
+ UserCreateCmd.Flags().String("password", "", "Required. The password for the new user account.")
+ UserCreateCmd.Flags().String("nickname", "", "Optional. The nickname for the new user account.")
+ UserCreateCmd.Flags().String("firstname", "", "Optional. The first name for the new user account.")
+ UserCreateCmd.Flags().String("lastname", "", "Optional. The last name for the new user account.")
+ UserCreateCmd.Flags().String("locale", "", "Optional. The locale (ex: en, fr) for the new user account.")
+ UserCreateCmd.Flags().Bool("system_admin", false, "Optional. If supplied, the new user will be a system administrator. Defaults to false.")
+
+ DeleteUserCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.")
+
+ DeleteAllUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.")
+
+ MigrateAuthCmd.Flags().Bool("force", false, "Force the migration to occur even if there are duplicates on the LDAP server. Duplicates will not be migrated. (ldap only)")
+ MigrateAuthCmd.Flags().Bool("auto", false, "Automatically migrate all users. Assumes the usernames and emails are identical between Mattermost and SAML services. (saml only)")
+ MigrateAuthCmd.Flags().Bool("dryRun", false, "Run a simulation of the migration process without changing the database.")
+ MigrateAuthCmd.SetUsageTemplate(`Usage:
platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags]
Examples:
@@ -184,7 +195,7 @@ Flags:
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}
`)
- migrateAuthCmd.SetHelpTemplate(`Usage:
+ MigrateAuthCmd.SetHelpTemplate(`Usage:
platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags]
Examples:
@@ -221,23 +232,25 @@ Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}
`)
- userCmd.AddCommand(
- userActivateCmd,
- userDeactivateCmd,
- userCreateCmd,
- userInviteCmd,
- resetUserPasswordCmd,
- resetUserMfaCmd,
- deleteUserCmd,
- deleteAllUsersCmd,
- migrateAuthCmd,
- verifyUserCmd,
- searchUserCmd,
+ UserCmd.AddCommand(
+ UserActivateCmd,
+ UserDeactivateCmd,
+ UserCreateCmd,
+ UserInviteCmd,
+ ResetUserPasswordCmd,
+ updateUserEmailCmd,
+ ResetUserMfaCmd,
+ DeleteUserCmd,
+ DeleteAllUsersCmd,
+ MigrateAuthCmd,
+ VerifyUserCmd,
+ SearchUserCmd,
)
+ cmd.RootCmd.AddCommand(UserCmd)
}
-func userActivateCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func userActivateCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -256,7 +269,7 @@ func changeUsersActiveStatus(a *app.App, userArgs []string, active bool) {
err := changeUserActiveStatus(a, user, userArgs[i], active)
if err != nil {
- CommandPrintErrorln(err.Error())
+ cmd.CommandPrintErrorln(err.Error())
}
}
}
@@ -275,8 +288,8 @@ func changeUserActiveStatus(a *app.App, user *model.User, userArg string, activa
return nil
}
-func userDeactivateCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func userDeactivateCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -289,29 +302,29 @@ func userDeactivateCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func userCreateCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func userCreateCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
- username, erru := cmd.Flags().GetString("username")
+ username, erru := command.Flags().GetString("username")
if erru != nil || username == "" {
return errors.New("Username is required")
}
- email, erre := cmd.Flags().GetString("email")
+ email, erre := command.Flags().GetString("email")
if erre != nil || email == "" {
return errors.New("Email is required")
}
- password, errp := cmd.Flags().GetString("password")
+ password, errp := command.Flags().GetString("password")
if errp != nil || password == "" {
return errors.New("Password is required")
}
- nickname, _ := cmd.Flags().GetString("nickname")
- firstname, _ := cmd.Flags().GetString("firstname")
- lastname, _ := cmd.Flags().GetString("lastname")
- locale, _ := cmd.Flags().GetString("locale")
- systemAdmin, _ := cmd.Flags().GetBool("system_admin")
+ nickname, _ := command.Flags().GetString("nickname")
+ firstname, _ := command.Flags().GetString("firstname")
+ lastname, _ := command.Flags().GetString("lastname")
+ locale, _ := command.Flags().GetString("locale")
+ systemAdmin, _ := command.Flags().GetBool("system_admin")
user := &model.User{
Username: username,
@@ -329,13 +342,13 @@ func userCreateCmdF(cmd *cobra.Command, args []string) error {
a.UpdateUserRoles(ruser.Id, "system_user system_admin", false)
}
- CommandPrettyPrintln("Created User")
+ cmd.CommandPrettyPrintln("Created User")
return nil
}
-func userInviteCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func userInviteCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -354,7 +367,7 @@ func userInviteCmdF(cmd *cobra.Command, args []string) error {
err := inviteUser(a, email, team, args[i+1])
if err != nil {
- CommandPrintErrorln(err.Error())
+ cmd.CommandPrintErrorln(err.Error())
}
}
@@ -368,13 +381,13 @@ func inviteUser(a *app.App, email string, team *model.Team, teamArg string) erro
}
a.SendInviteEmails(team, "Administrator", invites, *a.Config().ServiceSettings.SiteURL)
- CommandPrettyPrintln("Invites may or may not have been sent.")
+ cmd.CommandPrettyPrintln("Invites may or may not have been sent.")
return nil
}
-func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func resetUserPasswordCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -396,8 +409,38 @@ func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func resetUserMfaCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func updateUserEmailCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
+ if err != nil {
+ return err
+ }
+
+ newEmail := args[1]
+
+ if !model.IsValidEmail(newEmail) {
+ return errors.New("Invalid email: '" + newEmail + "'")
+ }
+
+ if len(args) != 2 {
+ return errors.New("Expected two arguments. See help text for details.")
+ }
+
+ user := getUserFromUserArg(a, args[0])
+ if user == nil {
+ return errors.New("Unable to find user '" + args[0] + "'")
+ }
+
+ user.Email = newEmail
+ _, errUpdate := a.UpdateUser(user, true)
+ if err != nil {
+ return errUpdate
+ }
+
+ return nil
+}
+
+func resetUserMfaCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -421,8 +464,8 @@ func resetUserMfaCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func deleteUserCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func deleteUserCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -431,16 +474,16 @@ func deleteUserCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Expected at least one argument. See help text for details.")
}
- confirmFlag, _ := cmd.Flags().GetBool("confirm")
+ confirmFlag, _ := command.Flags().GetBool("confirm")
if !confirmFlag {
var confirm string
- CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
+ cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
- CommandPrettyPrintln("Are you sure you want to permanently delete the specified users? (YES/NO): ")
+ cmd.CommandPrettyPrintln("Are you sure you want to permanently delete the specified users? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
@@ -462,8 +505,8 @@ func deleteUserCmdF(cmd *cobra.Command, args []string) error {
return nil
}
-func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func deleteAllUsersCommandF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -472,16 +515,16 @@ func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error {
return errors.New("Expected zero arguments.")
}
- confirmFlag, _ := cmd.Flags().GetBool("confirm")
+ confirmFlag, _ := command.Flags().GetBool("confirm")
if !confirmFlag {
var confirm string
- CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
+ cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
- CommandPrettyPrintln("Are you sure you want to permanently delete all user accounts? (YES/NO): ")
+ cmd.CommandPrettyPrintln("Are you sure you want to permanently delete all user accounts? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
@@ -492,19 +535,19 @@ func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error {
return err
}
- CommandPrettyPrintln("All user accounts successfully deleted.")
+ cmd.CommandPrettyPrintln("All user accounts successfully deleted.")
return nil
}
-func migrateAuthCmdF(cmd *cobra.Command, args []string) error {
+func migrateAuthCmdF(command *cobra.Command, args []string) error {
if args[1] == "saml" {
- return migrateAuthToSamlCmdF(cmd, args)
+ return migrateAuthToSamlCmdF(command, args)
}
- return migrateAuthToLdapCmdF(cmd, args)
+ return migrateAuthToLdapCmdF(command, args)
}
-func migrateAuthToLdapCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func migrateAuthToLdapCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -525,28 +568,28 @@ func migrateAuthToLdapCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Invalid match_field argument")
}
- forceFlag, _ := cmd.Flags().GetBool("force")
- dryRunFlag, _ := cmd.Flags().GetBool("dryRun")
+ forceFlag, _ := command.Flags().GetBool("force")
+ dryRunFlag, _ := command.Flags().GetBool("dryRun")
if migrate := a.AccountMigration; migrate != nil {
if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag, dryRunFlag); err != nil {
return errors.New("Error while migrating users: " + err.Error())
}
- CommandPrettyPrintln("Sucessfully migrated accounts.")
+ cmd.CommandPrettyPrintln("Sucessfully migrated accounts.")
}
return nil
}
-func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func migrateAuthToSamlCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
- dryRunFlag, _ := cmd.Flags().GetBool("dryRun")
- autoFlag, _ := cmd.Flags().GetBool("auto")
+ dryRunFlag, _ := command.Flags().GetBool("dryRun")
+ autoFlag, _ := command.Flags().GetBool("auto")
matchesFile := ""
matches := map[string]string{}
@@ -570,7 +613,7 @@ func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error {
if autoFlag && !dryRunFlag {
var confirm string
- CommandPrettyPrintln("You are about to perform an automatic \"" + fromAuth + " to saml\" migration. This must only be done if your current Mattermost users with " + fromAuth + " auth have the same username and email in your SAML service. Otherwise, provide the usernames and emails from your SAML Service using the \"users file\" without the \"--auto\" option.\n\nDo you want to proceed with automatic migration anyway? (YES/NO):")
+ cmd.CommandPrettyPrintln("You are about to perform an automatic \"" + fromAuth + " to saml\" migration. This must only be done if your current Mattermost users with " + fromAuth + " auth have the same username and email in your SAML service. Otherwise, provide the usernames and emails from your SAML Service using the \"users file\" without the \"--auto\" option.\n\nDo you want to proceed with automatic migration anyway? (YES/NO):")
fmt.Scanln(&confirm)
if confirm != "YES" {
@@ -588,14 +631,14 @@ func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Error while migrating users: " + err.Error())
}
l4g.Close()
- CommandPrettyPrintln("Sucessfully migrated accounts.")
+ cmd.CommandPrettyPrintln("Sucessfully migrated accounts.")
}
return nil
}
-func verifyUserCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func verifyUserCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -608,19 +651,19 @@ func verifyUserCmdF(cmd *cobra.Command, args []string) error {
for i, user := range users {
if user == nil {
- CommandPrintErrorln("Unable to find user '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to find user '" + args[i] + "'")
continue
}
if cresult := <-a.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil {
- CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error())
+ cmd.CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error())
}
}
return nil
}
-func searchUserCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func searchUserCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -633,21 +676,21 @@ func searchUserCmdF(cmd *cobra.Command, args []string) error {
for i, user := range users {
if i > 0 {
- CommandPrettyPrintln("------------------------------")
+ cmd.CommandPrettyPrintln("------------------------------")
}
if user == nil {
- CommandPrintErrorln("Unable to find user '" + args[i] + "'")
+ cmd.CommandPrintErrorln("Unable to find user '" + args[i] + "'")
continue
}
- CommandPrettyPrintln("id: " + user.Id)
- CommandPrettyPrintln("username: " + user.Username)
- CommandPrettyPrintln("nickname: " + user.Nickname)
- CommandPrettyPrintln("position: " + user.Position)
- CommandPrettyPrintln("first_name: " + user.FirstName)
- CommandPrettyPrintln("last_name: " + user.LastName)
- CommandPrettyPrintln("email: " + user.Email)
- CommandPrettyPrintln("auth_service: " + user.AuthService)
+ cmd.CommandPrettyPrintln("id: " + user.Id)
+ cmd.CommandPrettyPrintln("username: " + user.Username)
+ cmd.CommandPrettyPrintln("nickname: " + user.Nickname)
+ cmd.CommandPrettyPrintln("position: " + user.Position)
+ cmd.CommandPrettyPrintln("first_name: " + user.FirstName)
+ cmd.CommandPrettyPrintln("last_name: " + user.LastName)
+ cmd.CommandPrettyPrintln("email: " + user.Email)
+ cmd.CommandPrettyPrintln("auth_service: " + user.AuthService)
}
return nil
diff --git a/cmd/platform/user_test.go b/cmd/commands/user_test.go
index 5383ad914..a1081c5d3 100644
--- a/cmd/platform/user_test.go
+++ b/cmd/commands/user_test.go
@@ -1,13 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"testing"
"github.com/mattermost/mattermost-server/api"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
+ "github.com/stretchr/testify/require"
)
func TestCreateUserWithTeam(t *testing.T) {
@@ -18,9 +20,9 @@ func TestCreateUserWithTeam(t *testing.T) {
email := "success+" + id + "@simulator.amazonses.com"
username := "name" + id
- checkCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username)
+ cmd.CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username)
- checkCommand(t, "team", "add", th.SystemAdminTeam.Id, email)
+ cmd.CheckCommand(t, "team", "add", th.SystemAdminTeam.Id, email)
profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesInTeam(th.SystemAdminTeam.Id, 0, 1000, "")).Data.(map[string]*model.User)
@@ -46,7 +48,7 @@ func TestCreateUserWithoutTeam(t *testing.T) {
email := "success+" + id + "@simulator.amazonses.com"
username := "name" + id
- checkCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username)
+ cmd.CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username)
if result := <-th.App.Srv.Store.User().GetByEmail(email); result.Err != nil {
t.Fatal()
@@ -62,7 +64,7 @@ func TestResetPassword(t *testing.T) {
th := api.Setup().InitBasic()
defer th.TearDown()
- checkCommand(t, "user", "password", th.BasicUser.Email, "password2")
+ cmd.CheckCommand(t, "user", "password", th.BasicUser.Email, "password2")
th.BasicClient.Logout()
th.BasicUser.Password = "password2"
@@ -74,8 +76,35 @@ func TestMakeUserActiveAndInactive(t *testing.T) {
defer th.TearDown()
// first inactivate the user
- checkCommand(t, "user", "deactivate", th.BasicUser.Email)
+ cmd.CheckCommand(t, "user", "deactivate", th.BasicUser.Email)
// activate the inactive user
- checkCommand(t, "user", "activate", th.BasicUser.Email)
+ cmd.CheckCommand(t, "user", "activate", th.BasicUser.Email)
+}
+
+func TestChangeUserEmail(t *testing.T) {
+ th := api.Setup().InitBasic()
+ defer th.TearDown()
+
+ newEmail := model.NewId() + "@mattermost-test.com"
+
+ cmd.CheckCommand(t, "user", "email", th.BasicUser.Username, newEmail)
+ if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err == nil {
+ t.Fatal("should've updated to the new email")
+ }
+ if result := <-th.App.Srv.Store.User().GetByEmail(newEmail); result.Err != nil {
+ t.Fatal()
+ } else {
+ user := result.Data.(*model.User)
+ if user.Email != newEmail {
+ t.Fatal("should've updated to the new email")
+ }
+ }
+
+ // should fail because using an invalid email
+ require.Error(t, cmd.RunCommand(t, "user", "email", th.BasicUser.Username, "wrong$email.com"))
+
+ // should fail because user not found
+ require.Error(t, cmd.RunCommand(t, "user", "email", "invalidUser", newEmail))
+
}
diff --git a/cmd/platform/userargs.go b/cmd/commands/userargs.go
index 0089cc4da..ddeed6460 100644
--- a/cmd/platform/userargs.go
+++ b/cmd/commands/userargs.go
@@ -1,6 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"github.com/mattermost/mattermost-server/app"
diff --git a/cmd/platform/version.go b/cmd/commands/version.go
index 9616be1d7..eaf6a1a68 100644
--- a/cmd/platform/version.go
+++ b/cmd/commands/version.go
@@ -1,23 +1,29 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package commands
import (
"github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/cmd"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
"github.com/mattermost/mattermost-server/store/sqlstore"
"github.com/spf13/cobra"
)
-var versionCmd = &cobra.Command{
+var VersionCmd = &cobra.Command{
Use: "version",
Short: "Display version information",
RunE: versionCmdF,
}
-func versionCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
+func init() {
+ cmd.RootCmd.AddCommand(VersionCmd)
+}
+
+func versionCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
if err != nil {
return err
}
@@ -28,12 +34,12 @@ func versionCmdF(cmd *cobra.Command, args []string) error {
}
func printVersion(a *app.App) {
- CommandPrintln("Version: " + model.CurrentVersion)
- CommandPrintln("Build Number: " + model.BuildNumber)
- CommandPrintln("Build Date: " + model.BuildDate)
- CommandPrintln("Build Hash: " + model.BuildHash)
- CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady)
+ cmd.CommandPrintln("Version: " + model.CurrentVersion)
+ cmd.CommandPrintln("Build Number: " + model.BuildNumber)
+ cmd.CommandPrintln("Build Date: " + model.BuildDate)
+ cmd.CommandPrintln("Build Hash: " + model.BuildHash)
+ cmd.CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady)
if supplier, ok := a.Srv.Store.(*store.LayeredStore).DatabaseLayer.(*sqlstore.SqlSupplier); ok {
- CommandPrintln("DB Version: " + supplier.GetCurrentSchemaVersion())
+ cmd.CommandPrintln("DB Version: " + supplier.GetCurrentSchemaVersion())
}
}
diff --git a/cmd/platform/version_test.go b/cmd/commands/version_test.go
index eea2549ee..24a1389b1 100644
--- a/cmd/platform/version_test.go
+++ b/cmd/commands/version_test.go
@@ -1,12 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package commands
import (
"testing"
+
+ "github.com/mattermost/mattermost-server/cmd"
)
func TestVersion(t *testing.T) {
- checkCommand(t, "version")
+ cmd.CheckCommand(t, "version")
}
diff --git a/cmd/platform/init.go b/cmd/init.go
index ef3d78692..b71d71d31 100644
--- a/cmd/platform/init.go
+++ b/cmd/init.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+package cmd
import (
"github.com/mattermost/mattermost-server/app"
@@ -10,13 +10,13 @@ import (
"github.com/spf13/cobra"
)
-func initDBCommandContextCobra(cmd *cobra.Command) (*app.App, error) {
+func InitDBCommandContextCobra(cmd *cobra.Command) (*app.App, error) {
config, err := cmd.Flags().GetString("config")
if err != nil {
return nil, err
}
- a, err := initDBCommandContext(config)
+ a, err := InitDBCommandContext(config)
if err != nil {
// Returning an error just prints the usage message, so actually panic
panic(err)
@@ -25,7 +25,7 @@ func initDBCommandContextCobra(cmd *cobra.Command) (*app.App, error) {
return a, nil
}
-func initDBCommandContext(configFileLocation string) (*app.App, error) {
+func InitDBCommandContext(configFileLocation string) (*app.App, error) {
if err := utils.TranslationsPreInit(); err != nil {
return nil, err
}
diff --git a/cmd/platform/output.go b/cmd/output.go
index edf6ccc71..630e831de 100644
--- a/cmd/platform/output.go
+++ b/cmd/output.go
@@ -1,6 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package main
+
+package cmd
import (
"fmt"
diff --git a/cmd/platform/mattermost.go b/cmd/platform/mattermost.go
deleted file mode 100644
index e4a120e1e..000000000
--- a/cmd/platform/mattermost.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package main
-
-import (
- "errors"
- "fmt"
- "os"
-
- "github.com/spf13/cobra"
-
- // Plugins
- _ "github.com/mattermost/mattermost-server/model/gitlab"
-
- // Enterprise Imports
- _ "github.com/mattermost/mattermost-server/imports"
-
- // Enterprise Deps
- _ "github.com/dgryski/dgoogauth"
- _ "github.com/go-ldap/ldap"
- _ "github.com/hashicorp/memberlist"
- _ "github.com/mattermost/rsc/qr"
- _ "github.com/prometheus/client_golang/prometheus"
- _ "github.com/prometheus/client_golang/prometheus/promhttp"
- _ "github.com/tylerb/graceful"
- _ "gopkg.in/olivere/elastic.v5"
-
- // Temp imports for new dependencies
- _ "github.com/gorilla/schema"
-)
-
-func main() {
- if err := rootCmd.Execute(); err != nil {
- os.Exit(1)
- }
-}
-
-func init() {
- rootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.")
- rootCmd.PersistentFlags().Bool("disableconfigwatch", false, "When set config.json will not be loaded from disk when the file is changed.")
-
- resetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.")
-
- rootCmd.AddCommand(serverCmd, versionCmd, userCmd, teamCmd, licenseCmd, importCmd, resetCmd, channelCmd, rolesCmd, testCmd, ldapCmd, configCmd, jobserverCmd, commandCmd, messageExportCmd, sampleDataCmd)
-}
-
-var rootCmd = &cobra.Command{
- Use: "platform",
- Short: "Open source, self-hosted Slack-alternative",
- Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`,
- RunE: runServerCmd,
-}
-
-var resetCmd = &cobra.Command{
- Use: "reset",
- Short: "Reset the database to initial state",
- Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.",
- RunE: resetCmdF,
-}
-
-func resetCmdF(cmd *cobra.Command, args []string) error {
- a, err := initDBCommandContextCobra(cmd)
- if err != nil {
- return err
- }
-
- confirmFlag, _ := cmd.Flags().GetBool("confirm")
- if !confirmFlag {
- var confirm string
- CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
- fmt.Scanln(&confirm)
-
- if confirm != "YES" {
- return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
- }
- CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ")
- fmt.Scanln(&confirm)
- if confirm != "YES" {
- return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
- }
- }
-
- a.Srv.Store.DropAllTables()
- CommandPrettyPrintln("Database sucessfully reset")
-
- return nil
-}
diff --git a/config/default.json b/config/default.json
index 82b5fc57d..b05ff11e7 100644
--- a/config/default.json
+++ b/config/default.json
@@ -358,7 +358,13 @@
"DailyRunTime": "01:00",
"ExportFromTimestamp": 0,
"FileLocation": "export",
- "BatchSize": 10000
+ "BatchSize": 10000,
+ "GlobalRelaySettings": {
+ "CustomerType": "A9",
+ "SmtpUsername": "",
+ "SmtpPassword": "",
+ "EmailAddress": ""
+ }
},
"JobSettings": {
"RunJobs": true,
diff --git a/einterfaces/metrics.go b/einterfaces/metrics.go
index a88fe63cf..3f709eb99 100644
--- a/einterfaces/metrics.go
+++ b/einterfaces/metrics.go
@@ -29,8 +29,10 @@ type MetricsInterface interface {
IncrementMemCacheHitCounter(cacheName string)
IncrementMemCacheMissCounter(cacheName string)
+ IncrementMemCacheInvalidationCounter(cacheName string)
IncrementMemCacheMissCounterSession()
IncrementMemCacheHitCounterSession()
+ IncrementMemCacheInvalidationCounterSession()
IncrementWebsocketEvent(eventType string)
IncrementWebSocketBroadcast(eventType string)
diff --git a/glide.lock b/glide.lock
index 2efc6ba21..4349066e2 100644
--- a/glide.lock
+++ b/glide.lock
@@ -1,11 +1,13 @@
-hash: 6779beaa11fdb9c520471fb87c0a1a6ecc34a4c82610d942c44fba2f27a29936
-updated: 2018-02-15T18:28:32.209282461-08:00
+hash: 822849f55f8ab4b5c7545597b209edb6114bcf1009a552a9ee2503ff8d3fda09
+updated: 2018-03-07T13:01:49.575101746+01:00
imports:
- name: github.com/alecthomas/log4go
version: 3fbce08846379ec7f4f6bc7fce6dd01ce28fae4c
repo: https://github.com/mattermost/log4go.git
- name: github.com/armon/go-metrics
version: 7aa49fde808223f8dadfdbfd3a20ff6c19e5f9ec
+- name: github.com/avct/uasurfer
+ version: c4be5581ec9617d04f5c5e02b893903ead0b1eed
- name: github.com/beorn7/perks
version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9
subpackages:
@@ -138,8 +140,6 @@ imports:
version: b8bc1bf767474819792c23f32d8286a45736f1c6
- name: github.com/mitchellh/mapstructure
version: a4e142e9c047c904fa2f1e144d9a84e6133024bc
-- name: github.com/mssola/user_agent
- version: 5243daae23628aeae9b6268541406bd5e95d5964
- name: github.com/nicksnyder/go-i18n
version: 0dc1626d56435e9d605a29875701721c54bc9bbd
subpackages:
diff --git a/glide.yaml b/glide.yaml
index dc67106b4..02889a57b 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -77,3 +77,4 @@ import:
subpackages:
- store/memstore
- package: gopkg.in/yaml.v2
+- package: github.com/avct/uasurfer
diff --git a/i18n/de.json b/i18n/de.json
index b58cd73b1..35355919c 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -1369,6 +1369,10 @@
"translation": "Dateiupload nicht möglich. Header können nicht geparst werden."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "Datei über den maximalen Dimensionen konnte nicht hochgeladen werden: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " Eine oder mehrere Dateien in einer Direktnachricht hochgeladen"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " Eine oder mehrere Dateien hochgeladen in "
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " in "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Plugins wurden deaktiviert."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Kann Benutzer nicht auf AD/LDAP-Server finden: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "SAML Login war nicht erfolgreich da Verschlüsselung nicht aktiviert ist. Bitte kontaktieren Sie Ihren Systemadministrator."
},
@@ -5027,6 +5047,10 @@
"translation": "Ungültiger Wert für Webserver-Verbindungssicherheit."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "Die WebRTC-Gateway-Websocket-URL muss gesetzt und eine gültige URL sein sowie mit ws:// oder wss:// beginnen."
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Ungültiger Wert für write timeout."
},
diff --git a/i18n/en.json b/i18n/en.json
index bb906ae6c..85a09a139 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1831,14 +1831,14 @@
"translation": " uploaded one or more files in "
},
{
- "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
- "translation": " uploaded one or more files"
- },
- {
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
"translation": " uploaded one or more files in a direct message"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " uploaded one or more files"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " in "
},
@@ -4259,6 +4259,10 @@
"translation": "Unable to find user on AD/LDAP server: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
"id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
"translation": "User not found in the users file."
},
@@ -4267,10 +4271,6 @@
"translation": "Username already used by another Mattermost user."
},
{
- "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
- "translation": "Email already used by another SAML user."
- },
- {
"id": "ent.saml.attribute.app_error",
"translation": "SAML login was unsuccessful because one of the attributes is incorrect. Please contact your System Administrator."
},
@@ -4959,6 +4959,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."
},
@@ -5047,10 +5071,6 @@
"translation": "Site URL must be a valid URL and start with http:// or https://"
},
{
- "id": "model.config.is_valid.websocket_url.app_error",
- "translation": "Websocket URL must be a valid URL and start with ws:// or wss://"
- },
- {
"id": "model.config.is_valid.site_url_email_batching.app_error",
"translation": "Unable to enable email batching when SiteURL isn't set."
},
@@ -5119,6 +5139,10 @@
"translation": "Invalid value for webserver connection security."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "Websocket URL must be a valid URL and start with ws:// or wss://"
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Invalid value for write timeout."
},
diff --git a/i18n/es.json b/i18n/es.json
index fbb0c8538..e6a721f68 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -1369,6 +1369,10 @@
"translation": "No se puedo cargar el archivo. El encabezado no puede ser analizado."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "No se pudo subir los archivos. El número de archivos especificado es incorrecto."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "No se pudo cargar el archivo que supera las dimensiones máximas: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " subió uno o más archivos en un mensaje directo"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " subió uno o más archivos"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " en "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Los Plugins se han deshabilitado."
+ "translation": "Los Plugins han sido inhabilitados. Por favor revisa los logs para más detalles."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "No se puede encontrar el usuario en el servidor AD/LDAP: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Dirección de correo electrónico ya se encuentra en uso por otro usuario SAML."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "Usuario no encontrado en el archivo de usuarios."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Nombre de usuario ya se encuentra en uso por otro usuario de Mattermost."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "El inicio de sesión con SAML no tuvo éxito porque uno de sus atributos es incorrecto. Por favor, póngase en contacto con su Administrador del Sistema."
},
@@ -5027,6 +5047,10 @@
"translation": "Valor no válido para la seguridad de conexión del servidor."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "La dirección URL del Websocket debe ser una dirección válida y comenzar con ws:// o wss://."
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Valor no válido para el tiempo de espera de escritura."
},
diff --git a/i18n/fr.json b/i18n/fr.json
index 6b6071d41..3a9fae840 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -1369,6 +1369,10 @@
"translation": "Impossible d'envoyer le fichier. L'entête ne peut être analysé."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "Le fichier est au-dessus des limites de dimensions, il n'a pas pu être envoyé : {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " a envoyé un ou plusieurs fichiers dans un message personnel"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " a envoyé un ou plusieurs fichiers dans "
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " dans "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Les plugins ont été désactivés."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Impossible de trouver l'utilisateur sur le serveur AD/LDAP : "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "La connexion via SAML a échoué car un des attributs est incorrect. Veuillez contacter votre administrateur système."
},
@@ -5027,6 +5047,10 @@
"translation": "Valeur invalide pour la sécurité de la connexion au serveur web."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "URL de site invalide. Il doit s'agir d'une URL valide et commencer par http:// ou https://."
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Valeur invalide pour le délai d'attente d'écriture."
},
diff --git a/i18n/it.json b/i18n/it.json
index 7b6d3f077..0918ececa 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -1369,6 +1369,10 @@
"translation": "Impossibile caricare il file. Lettura dell'intestazione fallita."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Impossibile caricare i file. Specifica numero di file non valido."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "Non è stato possibile caricare il file che supera le dimensioni massime: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " ha caricato uno o più file in un messaggio diretto"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " ha caricato uno o più file"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " in "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Il plugin è stato disattivato."
+ "translation": "I plugin sono disattivati. Controllare i log per ulteriori informazioni."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Impossible trovare l'utente sul server AD/LDAP: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email in uso da un altro utente SAML."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "Utente non trovato nel file degli utenti."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Nome utente in uso da un altro utente Mattermost."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "Il login SAML non ha avuto successo a causa di un attributo non corretto. Contattare l'amministratore di sistema."
},
@@ -5027,6 +5047,10 @@
"translation": "Valore non valido per la sicurezza connessione webserver."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "L'URL del websocket deve essere un URL valido ed iniziare con ws:// o wss://"
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Valore non valido per il timeout scrittura."
},
diff --git a/i18n/ja.json b/i18n/ja.json
index b8fb048a5..b3e90c61f 100644
--- a/i18n/ja.json
+++ b/i18n/ja.json
@@ -1369,6 +1369,10 @@
"translation": "ファイルをアップロードできません。ヘッダーを解析できません。"
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "ファイルをアップロードできませんでした。指定されたファイル数が正しくありません。"
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "最大サイズ以上のファイルはアップロードできません: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " ファイルをダイレクトメッセージにアップロードしました"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " ファイルをアップロードしました"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " in "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "プラグインは無効化されています。"
+ "translation": "プラグインは無効化されています。詳しくはログを確認してください。"
},
{
"id": "app.plugin.extract.app_error",
@@ -3700,7 +3708,7 @@
},
{
"id": "app.team.join_user_to_team.max_accounts.app_error",
- "translation": "このチームは登録ユーザー数の上限に達しました。システム管理者に上限値の設定を変更するように依頼してください。"
+ "translation": "このチームは登録ユーザー数の上限に達しました。システム管理者に上限値を上げるよう依頼してください。"
},
{
"id": "app.user_access_token.disabled",
@@ -4183,6 +4191,18 @@
"translation": "AD/LDAPサーバー上でユーザを見つけることができませんでした: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "電子メールアドレスは既に別のSAMLユーザーによって使用されています。"
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "ユーザーはユーザーファイル内で見つかりませんでした。"
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "ユーザー名は既に別のMattermostユーザーによって使用されています。"
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "SAMLログインは、属性の一つが不正のため、失敗しました。システム管理者に連絡してください。"
},
@@ -5027,6 +5047,10 @@
"translation": "ウェブサーバーの接続のセキュリティーが不正な値です。"
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "ウェブソケットURLは、ws://またはwss://で始まる有効なURLにしてください。"
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "書き込みタイムアウトが不正な値です。"
},
diff --git a/i18n/ko.json b/i18n/ko.json
index b8eff6bca..79edba95c 100644
--- a/i18n/ko.json
+++ b/i18n/ko.json
@@ -1369,6 +1369,10 @@
"translation": "파일을 업로드 할 수 없습니다. 머릿말 파싱에 실패하였습니다."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "File above maximum dimensions could not be uploaded: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " uploaded one or more files in a direct message"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " uploaded one or more files"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " in "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Plugins have been disabled."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "AD/LDAP 서버에서 사용자를 찾을 수 없음: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "속성 중 하나가 올바르지 않아 SAML 로그인에 실패했습니다. 시스템 관리자에게 문의하십시오."
},
@@ -5027,6 +5047,10 @@
"translation": "webserver connection security에 대해 허용되지 않은 값입니다."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "WebRTC Gateway Websocket Url은 URL형식이여야 하며 ws:// 또는 wss://로 시작해야합니다."
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "write timeout에 대해 잘못된 값입니다."
},
diff --git a/i18n/nl.json b/i18n/nl.json
index a4ae85069..dda0ebab9 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -1369,6 +1369,10 @@
"translation": "Unable to upload file. Header cannot be parsed."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "File above maximum dimensions could not be uploaded: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " uploaded one or more files in a direct message"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " uploaded one or more files"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " in "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Plugins have been disabled."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Kon gebruiker niet vinden op de AD/LDAP server: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "SAML aanmelding is mislukt vanwege verkeerde attributen. Neem contact op met de beheerder."
},
@@ -5027,6 +5047,10 @@
"translation": "Invalid value for webserver connection security."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "WebRTC Gateway Websocket Url moet een geldige URL zijn en starten met ws:// of wss://."
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Invalid value for write timeout."
},
diff --git a/i18n/pl.json b/i18n/pl.json
index 209c00aeb..cf1911446 100644
--- a/i18n/pl.json
+++ b/i18n/pl.json
@@ -1369,6 +1369,10 @@
"translation": "Nie można pobrać pliku. Problem z parsowaniem nagłówka."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "Plik większy od maksymalnego rozmiaru nie został załadowany: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " Wysłał jeden lub więcej plików w bezpośredniej wiadomości"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " Wyślij jeden albo więcej plików w "
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": "w"
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Plugins have been disabled."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Nie można znaleźć użytkownika na serwerze AD/LDAP:"
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "Logowanie przez SAML nie powiodło się, ponieważ jeden z atrybutów jest niepoprawny. Skontaktuj się z administratorem systemu."
},
@@ -5027,6 +5047,10 @@
"translation": "Nieprawidłowa wartość zabezpieczenia połeczenia dla serwera web."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "Adres URL strony musi być prawidłowy i zaczynać się od http:// lub https://"
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Nieprawidłowa wartość upłynięcia limitu czasu."
},
diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json
index 314131254..44400b8c0 100644
--- a/i18n/pt-BR.json
+++ b/i18n/pt-BR.json
@@ -1369,6 +1369,10 @@
"translation": "Não foi possível carregar o arquivo. O cabeçalho não pode ser analisado."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "Arquivo acima das dimensões máximas não pode ser enviado: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " enviado um ou mais arquivos em uma mensagem direta"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " enviado um ou mais arquivos em "
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " em "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Os plugins foram desabilitados."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Não foi possível localizar usuário no servidor AD/LDAP: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "SAML login não foi bem sucedido porque um dos atributos está incorreto. Entre em contato com o Administrador do Sistema."
},
@@ -5027,6 +5047,10 @@
"translation": "Valor inválido para segurança de conexão do servidor web."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "A URL Websocket do WebRTC Gateway deve ser uma URL válida e começar com ws:// ou wss://."
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Valor inválido para limite de escrita."
},
diff --git a/i18n/ru.json b/i18n/ru.json
index 5d1b6be49..2d2f9d91f 100644
--- a/i18n/ru.json
+++ b/i18n/ru.json
@@ -1369,6 +1369,10 @@
"translation": "Невозможно загрузить файл. Заголовок не может быть распознан."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "Размер файла превышает максимальный размер и не может быть загружен: {{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " Загружены один или несколько файлов для сообщения"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " Загружено один или несколько файлов в "
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " в "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Plugins have been disabled."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Не удалось найти пользователя на сервере AD/LDAP: "
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "Попытка входа с использованием SAML не удалась из-за некорректного атрибута. Пожалуйста, свяжитесь с системным администратором."
},
@@ -5027,6 +5047,10 @@
"translation": "Недопустимое значение настроек безопасности соединения веб-сервера."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "Ссылка на шлюз WebRTC должна быть действующей и начинаться с ws:// или wss://."
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Недопустимое значение для времени ожидания записи."
},
diff --git a/i18n/tr.json b/i18n/tr.json
index d33570001..8eb6e20e2 100644
--- a/i18n/tr.json
+++ b/i18n/tr.json
@@ -1036,7 +1036,7 @@
},
{
"id": "api.context.mfa_required.app_error",
- "translation": "Bu sunucuda çok aşamalı kimlik doğrulaması kullanılıyor."
+ "translation": "Bu sunucuda çok aşamalı kimlik doğrulaması zorunludur."
},
{
"id": "api.context.missing_teamid.app_error",
@@ -1369,6 +1369,10 @@
"translation": "Dosya yüklenemedi. Üst bilgi işlenemedi."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Belirtilen dosya sayısı hatalı olduğundan dosyalar yüklenemedi."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "Dosya en büyük boyutları aştığından yüklenemedi: {{.Filename}}"
},
@@ -1808,13 +1812,17 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " şunun içine bir ya da bir kaç dosya yükledi "
+ "translation": " şuraya bir ya da bir kaç dosya yükledi "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
"translation": " bir doğrudan ileti içine bir ya da bir kaç dosya yükledi"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " bir ya da bir kaç dosya yükledi "
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " içinde "
},
@@ -2184,7 +2192,7 @@
},
{
"id": "api.team.add_user_to_team.missing_parameter.app_error",
- "translation": "Takıma kullanıcı eklemek için parametre gerekli."
+ "translation": "Takıma kullanıcı eklemek için parametre zorunludur."
},
{
"id": "api.team.create_team.email_disabled.app_error",
@@ -2300,11 +2308,11 @@
},
{
"id": "api.team.move_channel.post.error",
- "translation": "Kanal amacı iletisi gönderilemedi"
+ "translation": "Kanal taşındı iletisi gönderilemedi."
},
{
"id": "api.team.move_channel.success",
- "translation": "This channel has been moved to this team from %v."
+ "translation": "Bu kanal %v üzerinden bu takıma taşındı."
},
{
"id": "api.team.permanent_delete_team.attempting.warn",
@@ -3072,7 +3080,7 @@
},
{
"id": "api.webhook.incoming.error",
- "translation": "Could not decode the multipart payload of incoming webhook."
+ "translation": "Gelen web bağlantsının birden çok parçalı yükünün kodu çözülemedi."
},
{
"id": "api.webhook.init.debug",
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Uygulama ekleri devre dışı bırakılmış."
+ "translation": "Uygulama ekleri devre dışı bırakıldı. Ayrıntılar için günlük kayıtlarına bakın."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "Kullanıcı AD/LDAP sunucusu üzerinde bulunamadı:"
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "E-posta başka bir SAML kullanıcısı tarafından kullanılıyor."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "Kullanıcı, kullanıcılar dosyasında bulunamadı."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Bu kullanıcı adı başka bir Mattermost kullanıcısı tarafından kullanılıyor."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "Özniteliklerden biri hatalı olduğundan SAML oturumu açılamadı. Lütfen sistem yöneticiniz ile görüşün."
},
@@ -4884,7 +4904,7 @@
},
{
"id": "model.config.is_valid.message_export.export_type.app_error",
- "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ "translation": "İleti dışa aktarma görevinin Dışa Aktarma Biçimi 'actiance' ya da 'genelaktarım' olmalıdır"
},
{
"id": "model.config.is_valid.message_export.file_location.app_error",
@@ -4896,7 +4916,7 @@
},
{
"id": "model.config.is_valid.message_export.global_relay_email_address.app_error",
- "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address"
+ "translation": "İleti dışa aktarma görevinin GenelAktarıcıE-postaAdresi geçerli bir e-posta adresi olmalıdır"
},
{
"id": "model.config.is_valid.password_length.app_error",
@@ -5027,6 +5047,10 @@
"translation": "Web sunucusu bağlantı güvenliği değeri geçersiz."
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "Web soketi adresi ws:// ya da wss:// ile başlayan geçerli bir adres olmalıdır"
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "Yazma zaman aşımı değeri geçersiz."
},
@@ -7112,7 +7136,7 @@
},
{
"id": "utils.mail.sendMail.attachments.write_error",
- "translation": "Failed to write attachment to email"
+ "translation": "Ek dosya e-postaya eklenemedi"
},
{
"id": "utils.mail.send_mail.close.app_error",
diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json
index f799bba36..f476b48ed 100644
--- a/i18n/zh-CN.json
+++ b/i18n/zh-CN.json
@@ -1369,6 +1369,10 @@
"translation": "无法上传文件。标题无法被解析。"
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "无法上传文件。指定的文件数不匹配。"
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "无法上传超过最大尺寸的文件:{{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": " 在私信里上传一个或更多个文件"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " 已上传一个或更多个文件"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " 在 "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "插件已禁用。"
+ "translation": "日志已停用。请检查您的日志了解详情。"
},
{
"id": "app.plugin.extract.app_error",
@@ -3700,7 +3708,7 @@
},
{
"id": "app.team.join_user_to_team.max_accounts.app_error",
- "translation": "这个团队已经达到允许的最大用户数量。请与系统管理员联系以设置更高的限制。"
+ "translation": "这个团队已经达到允许的最大帐号数量。请与系统管理员联系以设置更高的限制。"
},
{
"id": "app.user_access_token.disabled",
@@ -4183,6 +4191,18 @@
"translation": "未在 AD/LDAP 服务器上找到用户:"
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "电子邮箱地址已被其他 SAML 用户使用。"
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "没有在用户文件里找到用户。"
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "用户名已被其他 Mattermost 用户使用。"
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "SAML登入因不正确属性而失败。请联系您的系统管理员。"
},
@@ -5027,6 +5047,10 @@
"translation": "错误的网页服务器连接安全值。"
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "Websocket 网址必须时有效的网址并且以 ws:// 或 wss:// 开头。"
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "错误的写入超时值。"
},
@@ -7112,7 +7136,7 @@
},
{
"id": "utils.mail.sendMail.attachments.write_error",
- "translation": "Failed to write attachment to email"
+ "translation": "给邮件添加附件失败"
},
{
"id": "utils.mail.send_mail.close.app_error",
diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json
index 292b05879..b9edc5edf 100644
--- a/i18n/zh-TW.json
+++ b/i18n/zh-TW.json
@@ -1369,6 +1369,10 @@
"translation": "無法上傳檔案。無法解析標頭。"
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "無法上傳超過最大尺寸的檔案:{{.Filename}}"
},
@@ -1815,6 +1819,10 @@
"translation": "在直接傳訊中已上傳一個或更多檔案"
},
{
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": "已上傳一個或更多檔案"
+ },
+ {
"id": "api.post.send_notifications_and_forget.push_in",
"translation": " 於 "
},
@@ -3648,7 +3656,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "模組已被停用。"
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -4183,6 +4191,18 @@
"translation": "找不到使用者,AD/LDAP 伺服器:"
},
{
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "由於不正確的屬性,SAML 登入失敗。請聯繫系統管理員。"
},
@@ -5027,6 +5047,10 @@
"translation": "網頁伺服器連線安全的值不正確。"
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "WebRTC 閘道 Websocket 網址必須是以 ws:// 或 wss:// 起始的有效網址。"
+ },
+ {
"id": "model.config.is_valid.write_timeout.app_error",
"translation": "寫入逾時的值不正確。"
},
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/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 8b17eaa7d..f12e4712b 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -699,7 +699,7 @@ func (c *Client4) GetUsersNotInTeam(teamId string, page int, perPage int, etag s
}
}
-// GetUsersInChannel returns a page of users on a team. Page counting starts at 0.
+// GetUsersInChannel returns a page of users in a channel. Page counting starts at 0.
func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage)
if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil {
@@ -710,7 +710,18 @@ func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, eta
}
}
-// GetUsersNotInChannel returns a page of users on a team. Page counting starts at 0.
+// GetUsersInChannelStatus returns a page of users in a channel. Page counting starts at 0. Sorted by Status
+func (c *Client4) GetUsersInChannelByStatus(channelId string, page int, perPage int, etag string) ([]*User, *Response) {
+ query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v&sort=status", channelId, page, perPage)
+ if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return UserListFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// GetUsersNotInChannel returns a page of users not in a channel. Page counting starts at 0.
func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?in_team=%v&not_in_channel=%v&page=%v&per_page=%v", teamId, channelId, page, perPage)
if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil {
diff --git a/model/cluster_info.go b/model/cluster_info.go
index a8d63ec32..46a3487a9 100644
--- a/model/cluster_info.go
+++ b/model/cluster_info.go
@@ -6,7 +6,6 @@ package model
import (
"encoding/json"
"io"
- "strings"
)
type ClusterInfo struct {
@@ -22,11 +21,6 @@ func (me *ClusterInfo) ToJson() string {
return string(b)
}
-func (me *ClusterInfo) Copy() *ClusterInfo {
- json := me.ToJson()
- return ClusterInfoFromJson(strings.NewReader(json))
-}
-
func ClusterInfoFromJson(data io.Reader) *ClusterInfo {
var me *ClusterInfo
json.NewDecoder(data).Decode(&me)
diff --git a/model/compliance_post.go b/model/compliance_post.go
index 3751c5862..75e8de1f1 100644
--- a/model/compliance_post.go
+++ b/model/compliance_post.go
@@ -17,6 +17,7 @@ type CompliancePost struct {
// From Channel
ChannelName string
ChannelDisplayName string
+ ChannelType string
// From User
UserUsername string
@@ -45,6 +46,7 @@ func CompliancePostHeader() []string {
"ChannelName",
"ChannelDisplayName",
+ "ChannelType",
"UserUsername",
"UserEmail",
@@ -92,6 +94,7 @@ func (me *CompliancePost) Row() []string {
cleanComplianceStrings(me.ChannelName),
cleanComplianceStrings(me.ChannelDisplayName),
+ cleanComplianceStrings(me.ChannelType),
cleanComplianceStrings(me.UserUsername),
cleanComplianceStrings(me.UserEmail),
diff --git a/model/config.go b/model/config.go
index c8cd0f0a1..1d5e06fca 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 {
@@ -1638,6 +1633,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
@@ -1646,7 +1663,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() {
@@ -1677,6 +1694,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
@@ -2210,10 +2232,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 5ba4854b6..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"`
@@ -173,6 +164,11 @@ func (m *Manifest) ClientManifest() *Manifest {
cm.Name = ""
cm.Description = ""
cm.Backend = nil
+ if cm.Webapp != nil {
+ cm.Webapp = new(ManifestWebapp)
+ *cm.Webapp = *m.Webapp
+ cm.Webapp.BundlePath = "/static/" + m.Id + "_bundle.js"
+ }
return cm
}
diff --git a/model/manifest_test.go b/model/manifest_test.go
index 3fdc13ec4..b63e388bc 100644
--- a/model/manifest_test.go
+++ b/model/manifest_test.go
@@ -74,7 +74,7 @@ func TestManifestUnmarshal(t *testing.T) {
&PluginSetting{
Key: "thesetting",
DisplayName: "thedisplayname",
- Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
+ Type: "dropdown",
HelpText: "thehelptext",
RegenerateHelpText: "theregeneratehelptext",
Placeholder: "theplaceholder",
@@ -181,7 +181,7 @@ func TestManifestJson(t *testing.T) {
&PluginSetting{
Key: "thesetting",
DisplayName: "thedisplayname",
- Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
+ Type: "dropdown",
HelpText: "thehelptext",
RegenerateHelpText: "theregeneratehelptext",
Placeholder: "theplaceholder",
@@ -246,7 +246,7 @@ func TestManifestClientManifest(t *testing.T) {
&PluginSetting{
Key: "thesetting",
DisplayName: "thedisplayname",
- Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
+ Type: "dropdown",
HelpText: "thehelptext",
RegenerateHelpText: "theregeneratehelptext",
Placeholder: "theplaceholder",
diff --git a/model/message_export.go b/model/message_export.go
index 22641deee..6efb8c6a4 100644
--- a/model/message_export.go
+++ b/model/message_export.go
@@ -6,6 +6,7 @@ package model
type MessageExport struct {
ChannelId *string
ChannelDisplayName *string
+ ChannelType *string
UserId *string
UserEmail *string
diff --git a/model/oauth.go b/model/oauth.go
index 70e8a3f26..c92b1ec41 100644
--- a/model/oauth.go
+++ b/model/oauth.go
@@ -141,17 +141,6 @@ func OAuthAppFromJson(data io.Reader) *OAuthApp {
return app
}
-func OAuthAppMapToJson(a map[string]*OAuthApp) string {
- b, _ := json.Marshal(a)
- return string(b)
-}
-
-func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp {
- var apps map[string]*OAuthApp
- json.NewDecoder(data).Decode(&apps)
- return apps
-}
-
func OAuthAppListToJson(l []*OAuthApp) string {
b, _ := json.Marshal(l)
return string(b)
diff --git a/model/saml.go b/model/saml.go
index e74750156..528ac45cc 100644
--- a/model/saml.go
+++ b/model/saml.go
@@ -11,9 +11,6 @@ import (
const (
USER_AUTH_SERVICE_SAML = "saml"
USER_AUTH_SERVICE_SAML_TEXT = "With SAML"
- SAML_IDP_CERTIFICATE = 1
- SAML_PRIVATE_KEY = 2
- SAML_PUBLIC_CERT = 3
)
type SamlAuthRequest struct {
diff --git a/model/search_params.go b/model/search_params.go
index 1692b3aaf..481671ab5 100644
--- a/model/search_params.go
+++ b/model/search_params.go
@@ -4,7 +4,6 @@
package model
import (
- "encoding/json"
"regexp"
"strings"
)
@@ -20,11 +19,6 @@ type SearchParams struct {
OrTerms bool
}
-func (o *SearchParams) ToJson() string {
- b, _ := json.Marshal(o)
- return string(b)
-}
-
var searchFlags = [...]string{"from", "channel", "in"}
func splitWords(text string) []string {
diff --git a/model/team.go b/model/team.go
index 15a708220..7968c9d48 100644
--- a/model/team.go
+++ b/model/team.go
@@ -243,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 d915307ad..59472d8e5 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 6e461e5d5..e4e0af491 100644
--- a/model/version.go
+++ b/model/version.go
@@ -13,6 +13,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
+ "4.8.0",
"4.7.1",
"4.7.0",
"4.6.0",
@@ -106,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 aea77b1b6..9013ab428 100644
--- a/model/websocket_message.go
+++ b/model/websocket_message.go
@@ -45,6 +45,8 @@ const (
WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE
WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE
WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated"
+ WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed"
+ WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed"
)
type WebSocketMessage interface {
diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go
index f53021f74..adc9ddbde 100644
--- a/plugin/pluginenv/environment.go
+++ b/plugin/pluginenv/environment.go
@@ -8,6 +8,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
+ "path/filepath"
"sync"
"github.com/pkg/errors"
@@ -163,12 +164,24 @@ func (env *Environment) ActivatePlugin(id string) error {
return fmt.Errorf("env missing webapp path, cannot activate plugin: %v", id)
}
- webappBundle, err := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id))
+ bundlePath := filepath.Clean(bundle.Manifest.Webapp.BundlePath)
+ if bundlePath == "" || bundlePath[0] == '.' {
+ return fmt.Errorf("invalid webapp bundle path")
+ }
+ bundlePath = filepath.Join(env.searchPath, id, bundlePath)
+
+ webappBundle, err := ioutil.ReadFile(bundlePath)
if err != nil {
- if supervisor != nil {
- supervisor.Stop()
+ // Backwards compatibility for plugins where webapp.bundle_path was ignored. This should
+ // be removed eventually.
+ if webappBundle2, err2 := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id)); err2 == nil {
+ webappBundle = webappBundle2
+ } else {
+ if supervisor != nil {
+ supervisor.Stop()
+ }
+ return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
}
- return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
}
err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPath, id), webappBundle, 0644)
diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go
index 5b5b11a62..d87f65b55 100644
--- a/plugin/rpcplugin/api.go
+++ b/plugin/rpcplugin/api.go
@@ -610,4 +610,6 @@ func ConnectAPI(conn io.ReadWriteCloser, muxer *Muxer) *RemoteAPI {
func init() {
gob.Register([]*model.SlackAttachment{})
+ gob.Register([]interface{}{})
+ gob.Register(map[string]interface{}{})
}
diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go
index 145ec9005..7fe7a0ff9 100644
--- a/plugin/rpcplugin/api_test.go
+++ b/plugin/rpcplugin/api_test.go
@@ -72,11 +72,6 @@ func TestAPI(t *testing.T) {
testPost := &model.Post{
Message: "hello",
- Props: map[string]interface{}{
- "attachments": []*model.SlackAttachment{
- &model.SlackAttachment{},
- },
- },
}
testAPIRPC(&api, func(remote plugin.API) {
@@ -244,3 +239,41 @@ func TestAPI(t *testing.T) {
assert.Nil(t, err)
})
}
+
+func TestAPI_GobRegistration(t *testing.T) {
+ keyValueStore := &plugintest.KeyValueStore{}
+ api := plugintest.API{Store: keyValueStore}
+ defer api.AssertExpectations(t)
+
+ testAPIRPC(&api, func(remote plugin.API) {
+ api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) (*model.Post, *model.AppError) {
+ p.Id = "thepostid"
+ return p, nil
+ }).Once()
+ _, err := remote.CreatePost(&model.Post{
+ Message: "hello",
+ Props: map[string]interface{}{
+ "attachments": []*model.SlackAttachment{
+ &model.SlackAttachment{
+ Actions: []*model.PostAction{
+ &model.PostAction{
+ Integration: &model.PostActionIntegration{
+ Context: map[string]interface{}{
+ "foo": "bar",
+ "foos": []interface{}{"bar", "baz", 1, 2},
+ "foo_map": map[string]interface{}{
+ "1": "bar",
+ "2": 2,
+ },
+ },
+ },
+ },
+ },
+ Timestamp: 1,
+ },
+ },
+ },
+ })
+ require.Nil(t, err)
+ })
+}
diff --git a/plugin/rpcplugin/sandbox/sandbox_linux.go b/plugin/rpcplugin/sandbox/sandbox_linux.go
index c83572c82..4ade00cf2 100644
--- a/plugin/rpcplugin/sandbox/sandbox_linux.go
+++ b/plugin/rpcplugin/sandbox/sandbox_linux.go
@@ -425,6 +425,15 @@ func checkSupportInNamespace() error {
return errors.Wrapf(err, "unable to enable seccomp filter")
}
+ if f, err := os.Create(os.DevNull); err != nil {
+ return errors.Wrapf(err, "unable to open os.DevNull")
+ } else {
+ defer f.Close()
+ if _, err = f.Write([]byte("foo")); err != nil {
+ return errors.Wrapf(err, "unable to write to os.DevNull")
+ }
+ }
+
return nil
}
diff --git a/store/layered_store_supplier.go b/store/layered_store_supplier.go
index 482ccd126..80fe3cb24 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 131e5649b..e7a157192 100644
--- a/store/sqlstore/channel_store.go
+++ b/store/sqlstore/channel_store.go
@@ -43,12 +43,20 @@ var allChannelMembersNotifyPropsForChannelCache = utils.NewLru(ALL_CHANNEL_MEMBE
var channelCache = utils.NewLru(model.CHANNEL_CACHE_SIZE)
var channelByNameCache = utils.NewLru(model.CHANNEL_CACHE_SIZE)
-func ClearChannelCaches() {
+func (s SqlChannelStore) ClearCaches() {
channelMemberCountsCache.Purge()
allChannelMembersForUserCache.Purge()
allChannelMembersNotifyPropsForChannelCache.Purge()
channelCache.Purge()
channelByNameCache.Purge()
+
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Purge")
+ s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Purge")
+ s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Purge")
+ s.metrics.IncrementMemCacheInvalidationCounter("Channel - Purge")
+ s.metrics.IncrementMemCacheInvalidationCounter("Channel By Name - Purge")
+ }
}
func NewSqlChannelStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.ChannelStore {
@@ -308,12 +316,18 @@ func (s SqlChannelStore) GetChannelUnread(channelId, userId string) store.StoreC
})
}
-func (us SqlChannelStore) InvalidateChannel(id string) {
+func (s SqlChannelStore) InvalidateChannel(id string) {
channelCache.Remove(id)
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Channel - Remove by ChannelId")
+ }
}
-func (us SqlChannelStore) InvalidateChannelByName(teamId, name string) {
+func (s SqlChannelStore) InvalidateChannelByName(teamId, name string) {
channelByNameCache.Remove(teamId + name)
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Channel by Name - Remove by TeamId and Name")
+ }
}
func (s SqlChannelStore) Get(id string, allowFromCache bool) store.StoreChannel {
@@ -814,14 +828,17 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) store.StoreC
})
}
-func (us SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
+func (s SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
allChannelMembersForUserCache.Remove(userId)
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Remove by UserId")
+ }
}
-func (us SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool {
+func (s SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool {
if cacheItem, ok := allChannelMembersForUserCache.Get(userId); ok {
- if us.metrics != nil {
- us.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
}
ids := cacheItem.(map[string]string)
if _, ok := ids[channelId]; ok {
@@ -830,12 +847,12 @@ func (us SqlChannelStore) IsUserInChannelUseCache(userId string, channelId strin
return false
}
} else {
- if us.metrics != nil {
- us.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
}
}
- if result := <-us.GetAllChannelMembersForUser(userId, true); result.Err != nil {
+ if result := <-s.GetAllChannelMembersForUser(userId, true); result.Err != nil {
l4g.Error("SqlChannelStore.IsUserInChannelUseCache: " + result.Err.Error())
return false
} else {
@@ -915,8 +932,11 @@ func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCac
})
}
-func (us SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
+func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
allChannelMembersNotifyPropsForChannelCache.Remove(channelId)
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Remove by ChannelId")
+ }
}
type allChannelMemberNotifyProps struct {
@@ -966,8 +986,11 @@ func (s SqlChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId str
})
}
-func (us SqlChannelStore) InvalidateMemberCount(channelId string) {
+func (s SqlChannelStore) InvalidateMemberCount(channelId string) {
channelMemberCountsCache.Remove(channelId)
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Remove by ChannelId")
+ }
}
func (s SqlChannelStore) GetMemberCountFromCache(channelId string) int64 {
@@ -1225,19 +1248,6 @@ func (s SqlChannelStore) AnalyticsDeletedTypeCount(teamId string, channelType st
})
}
-func (s SqlChannelStore) ExtraUpdateByUser(userId string, time int64) store.StoreChannel {
- return store.Do(func(result *store.StoreResult) {
- _, err := s.GetMaster().Exec(
- `UPDATE Channels SET ExtraUpdateAt = :Time
- WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId);`,
- map[string]interface{}{"UserId": userId, "Time": time})
-
- if err != nil {
- result.Err = model.NewAppError("SqlChannelStore.extraUpdated", "store.sql_channel.extra_updated.app_error", nil, "user_id="+userId+", "+err.Error(), http.StatusInternalServerError)
- }
- })
-}
-
func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
members := &model.ChannelMembers{}
diff --git a/store/sqlstore/compliance_store.go b/store/sqlstore/compliance_store.go
index 03d92d5e1..c3c75581e 100644
--- a/store/sqlstore/compliance_store.go
+++ b/store/sqlstore/compliance_store.go
@@ -138,6 +138,7 @@ func (s SqlComplianceStore) ComplianceExport(job *model.Compliance) store.StoreC
Teams.DisplayName AS TeamDisplayName,
Channels.Name AS ChannelName,
Channels.DisplayName AS ChannelDisplayName,
+ Channels.Type AS ChannelType,
Users.Username AS UserUsername,
Users.Email AS UserEmail,
Users.Nickname AS UserNickname,
@@ -172,6 +173,7 @@ func (s SqlComplianceStore) ComplianceExport(job *model.Compliance) store.StoreC
'Direct Messages' AS TeamDisplayName,
Channels.Name AS ChannelName,
Channels.DisplayName AS ChannelDisplayName,
+ Channels.Type AS ChannelType,
Users.Username AS UserUsername,
Users.Email AS UserEmail,
Users.Nickname AS UserNickname,
@@ -223,7 +225,12 @@ func (s SqlComplianceStore) MessageExport(after int64, limit int) store.StoreCha
Posts.Type AS PostType,
Posts.FileIds AS PostFileIds,
Channels.Id AS ChannelId,
- Channels.DisplayName AS ChannelDisplayName,
+ CASE
+ WHEN Channels.Type = 'D' THEN 'Direct Message'
+ WHEN Channels.Type = 'G' THEN 'Group Message'
+ ELSE Channels.DisplayName
+ END AS ChannelDisplayName,
+ Channels.Type AS ChannelType,
Users.Id AS UserId,
Users.Email AS UserEmail,
Users.Username
diff --git a/store/sqlstore/file_info_store.go b/store/sqlstore/file_info_store.go
index 1d0767d1e..7559640c8 100644
--- a/store/sqlstore/file_info_store.go
+++ b/store/sqlstore/file_info_store.go
@@ -25,8 +25,11 @@ const (
var fileInfoCache *utils.Cache = utils.NewLru(FILE_INFO_CACHE_SIZE)
-func ClearFileCaches() {
+func (fs SqlFileInfoStore) ClearCaches() {
fileInfoCache.Purge()
+ if fs.metrics != nil {
+ fs.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Purge")
+ }
}
func NewSqlFileInfoStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.FileInfoStore {
@@ -118,6 +121,9 @@ func (fs SqlFileInfoStore) GetByPath(path string) store.StoreChannel {
func (fs SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string) {
fileInfoCache.Remove(postId)
+ if fs.metrics != nil {
+ fs.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Remove by PostId")
+ }
}
func (fs SqlFileInfoStore) GetForPost(postId string, readFromMaster bool, allowFromCache bool) store.StoreChannel {
diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go
index 25c3c4913..92ee28ffa 100644
--- a/store/sqlstore/post_store.go
+++ b/store/sqlstore/post_store.go
@@ -35,9 +35,14 @@ const (
var lastPostTimeCache = utils.NewLru(LAST_POST_TIME_CACHE_SIZE)
var lastPostsCache = utils.NewLru(LAST_POSTS_CACHE_SIZE)
-func ClearPostCaches() {
+func (s SqlPostStore) ClearCaches() {
lastPostTimeCache.Purge()
lastPostsCache.Purge()
+
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Purge")
+ s.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Purge")
+ }
}
func NewSqlPostStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.PostStore {
@@ -326,6 +331,11 @@ func (s SqlPostStore) InvalidateLastPostTimeCache(channelId string) {
// Keys are "{channelid}{limit}" and caching only occurs on limits of 30 and 60
lastPostsCache.Remove(channelId + "30")
lastPostsCache.Remove(channelId + "60")
+
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Remove by Channel Id")
+ s.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Remove by Channel Id")
+ }
}
func (s SqlPostStore) GetEtag(channelId string, allowFromCache bool) store.StoreChannel {
diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go
index de4dbe095..2b4532817 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.
@@ -365,13 +367,19 @@ func UpgradeDatabaseToVersion471(sqlStore SqlStore) {
}
func UpgradeDatabaseToVersion48(sqlStore SqlStore) {
+ if shouldPerformUpgrade(sqlStore, VERSION_4_7_1, VERSION_4_8_0) {
+ saveSchemaVersion(sqlStore, VERSION_4_8_0)
+ }
+}
+
+func UpgradeDatabaseToVersion49(sqlStore SqlStore) {
// This version of Mattermost includes an App-Layer migration which migrates from hard-coded roles configured by
// a number of parameters in `config.json` to a `Roles` table in the database. The migration code can be seen
// in the file `app/app.go` in the function `DoAdvancedPermissionsMigration()`.
- //TODO: Uncomment the following condition when version 4.8.0 is released
- //if shouldPerformUpgrade(sqlStore, VERSION_4_7_0, VERSION_4_8_0) {
+ //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_8_0)
+ // saveSchemaVersion(sqlStore, VERSION_4_9_0)
//}
}
diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go
index d67a45704..5e84af930 100644
--- a/store/sqlstore/user_store.go
+++ b/store/sqlstore/user_store.go
@@ -38,13 +38,22 @@ type SqlUserStore struct {
var profilesInChannelCache *utils.Cache = utils.NewLru(PROFILES_IN_CHANNEL_CACHE_SIZE)
var profileByIdsCache *utils.Cache = utils.NewLru(PROFILE_BY_IDS_CACHE_SIZE)
-func ClearUserCaches() {
+func (us SqlUserStore) ClearCaches() {
profilesInChannelCache.Purge()
profileByIdsCache.Purge()
+
+ if us.metrics != nil {
+ us.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Purge")
+ us.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Purge")
+ }
}
func (us SqlUserStore) InvalidatProfileCacheForUser(userId string) {
profileByIdsCache.Remove(userId)
+
+ if us.metrics != nil {
+ us.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Remove")
+ }
}
func NewSqlUserStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.UserStore {
@@ -384,6 +393,9 @@ func (us SqlUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {
userMap := cacheItem.(map[string]*model.User)
if _, userInCache := userMap[userId]; userInCache {
profilesInChannelCache.Remove(key)
+ if us.metrics != nil {
+ us.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by User")
+ }
}
}
}
@@ -391,13 +403,27 @@ func (us SqlUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {
func (us SqlUserStore) InvalidateProfilesInChannelCache(channelId string) {
profilesInChannelCache.Remove(channelId)
+ if us.metrics != nil {
+ us.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by Channel")
+ }
}
func (us SqlUserStore) GetProfilesInChannel(channelId string, offset int, limit int) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
var users []*model.User
- query := "SELECT Users.* FROM Users, ChannelMembers WHERE ChannelMembers.ChannelId = :ChannelId AND Users.Id = ChannelMembers.UserId ORDER BY Users.Username ASC LIMIT :Limit OFFSET :Offset"
+ query := `
+ SELECT
+ Users.*
+ FROM
+ Users, ChannelMembers
+ WHERE
+ ChannelMembers.ChannelId = :ChannelId
+ AND Users.Id = ChannelMembers.UserId
+ ORDER BY
+ Users.Username ASC
+ LIMIT :Limit OFFSET :Offset
+ `
if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewAppError("SqlUserStore.GetProfilesInChannel", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
@@ -412,6 +438,42 @@ func (us SqlUserStore) GetProfilesInChannel(channelId string, offset int, limit
})
}
+func (us SqlUserStore) GetProfilesInChannelByStatus(channelId string, offset int, limit int) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ var users []*model.User
+
+ query := `
+ SELECT
+ Users.*
+ FROM Users
+ INNER JOIN ChannelMembers ON Users.Id = ChannelMembers.UserId
+ LEFT JOIN Status ON Users.Id = Status.UserId
+ WHERE
+ ChannelMembers.ChannelId = :ChannelId
+ ORDER BY
+ CASE Status
+ WHEN 'online' THEN 1
+ WHEN 'away' THEN 2
+ WHEN 'dnd' THEN 3
+ ELSE 4
+ END,
+ Users.Username ASC
+ LIMIT :Limit OFFSET :Offset
+ `
+
+ if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetProfilesInChannelByStatus", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
+ } else {
+
+ for _, u := range users {
+ u.Sanitize(map[string]bool{})
+ }
+
+ result.Data = users
+ }
+ })
+}
+
func (us SqlUserStore) GetAllProfilesInChannel(channelId string, allowFromCache bool) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
if allowFromCache {
diff --git a/store/sqlstore/webhook_store.go b/store/sqlstore/webhook_store.go
index 8a3720fa0..45ad90e52 100644
--- a/store/sqlstore/webhook_store.go
+++ b/store/sqlstore/webhook_store.go
@@ -26,8 +26,12 @@ const (
var webhookCache = utils.NewLru(WEBHOOK_CACHE_SIZE)
-func ClearWebhookCaches() {
+func (s SqlWebhookStore) ClearCaches() {
webhookCache.Purge()
+
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Webhook - Purge")
+ }
}
func NewSqlWebhookStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.WebhookStore {
@@ -78,6 +82,9 @@ func (s SqlWebhookStore) CreateIndexesIfNotExists() {
func (s SqlWebhookStore) InvalidateWebhookCache(webhookId string) {
webhookCache.Remove(webhookId)
+ if s.metrics != nil {
+ s.metrics.IncrementMemCacheInvalidationCounter("Webhook - Remove by WebhookId")
+ }
}
func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) store.StoreChannel {
@@ -164,7 +171,7 @@ func (s SqlWebhookStore) PermanentDeleteIncomingByUser(userId string) store.Stor
result.Err = model.NewAppError("SqlWebhookStore.DeleteIncomingByUser", "store.sql_webhooks.permanent_delete_incoming_by_user.app_error", nil, "id="+userId+", err="+err.Error(), http.StatusInternalServerError)
}
- ClearWebhookCaches()
+ s.ClearCaches()
})
}
@@ -175,7 +182,7 @@ func (s SqlWebhookStore) PermanentDeleteIncomingByChannel(channelId string) stor
result.Err = model.NewAppError("SqlWebhookStore.DeleteIncomingByChannel", "store.sql_webhooks.permanent_delete_incoming_by_channel.app_error", nil, "id="+channelId+", err="+err.Error(), http.StatusInternalServerError)
}
- ClearWebhookCaches()
+ s.ClearCaches()
})
}
@@ -322,7 +329,7 @@ func (s SqlWebhookStore) PermanentDeleteOutgoingByChannel(channelId string) stor
result.Err = model.NewAppError("SqlWebhookStore.DeleteOutgoingByChannel", "store.sql_webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "id="+channelId+", err="+err.Error(), http.StatusInternalServerError)
}
- ClearWebhookCaches()
+ s.ClearCaches()
})
}
diff --git a/store/store.go b/store/store.go
index 5da91c071..a4b66d2d1 100644
--- a/store/store.go
+++ b/store/store.go
@@ -154,7 +154,6 @@ type ChannelStore interface {
UpdateLastViewedAt(channelIds []string, userId string) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
- ExtraUpdateByUser(userId string, time int64) StoreChannel
GetMembersForUser(teamId string, userId string) StoreChannel
AutocompleteInTeam(teamId string, term string) StoreChannel
SearchInTeam(teamId string, term string) StoreChannel
@@ -162,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 {
@@ -191,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
@@ -211,9 +212,11 @@ type UserStore interface {
UpdateMfaActive(userId string, active bool) StoreChannel
Get(id string) StoreChannel
GetAll() StoreChannel
+ ClearCaches()
InvalidateProfilesInChannelCacheByUser(userId string)
InvalidateProfilesInChannelCache(channelId string)
GetProfilesInChannel(channelId string, offset int, limit int) StoreChannel
+ GetProfilesInChannelByStatus(channelId string, offset int, limit int) StoreChannel
GetAllProfilesInChannel(channelId string, allowFromCache bool) StoreChannel
GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel
GetProfilesWithoutTeam(offset int, limit int) StoreChannel
@@ -344,6 +347,7 @@ type WebhookStore interface {
AnalyticsIncomingCount(teamId string) StoreChannel
AnalyticsOutgoingCount(teamId string) StoreChannel
InvalidateWebhookCache(webhook string)
+ ClearCaches()
}
type CommandStore interface {
@@ -421,6 +425,7 @@ type FileInfoStore interface {
DeleteForPost(postId string) StoreChannel
PermanentDelete(fileId string) StoreChannel
PermanentDeleteBatch(endTime int64, limit int64) StoreChannel
+ ClearCaches()
}
type ReactionStore interface {
diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go
index 121b40a01..d3b69edea 100644
--- a/store/storetest/channel_store.go
+++ b/store/storetest/channel_store.go
@@ -43,7 +43,6 @@ func TestChannelStore(t *testing.T, ss store.Store) {
t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) })
t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) })
t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) })
- t.Run("UpdateExtrasByUser", func(t *testing.T) { testUpdateExtrasByUser(t, ss) })
t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) })
t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) })
t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) })
@@ -1641,54 +1640,6 @@ func testGetMemberCount(t *testing.T, ss store.Store) {
}
}
-func testUpdateExtrasByUser(t *testing.T, ss store.Store) {
- teamId := model.NewId()
-
- c1 := model.Channel{
- TeamId: teamId,
- DisplayName: "Channel1",
- Name: "zz" + model.NewId() + "b",
- Type: model.CHANNEL_OPEN,
- }
- store.Must(ss.Channel().Save(&c1, -1))
-
- c2 := model.Channel{
- TeamId: teamId,
- DisplayName: "Channel2",
- Name: "zz" + model.NewId() + "b",
- Type: model.CHANNEL_OPEN,
- }
- store.Must(ss.Channel().Save(&c2, -1))
-
- u1 := &model.User{
- Email: model.NewId(),
- DeleteAt: 0,
- }
- store.Must(ss.User().Save(u1))
- store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1))
-
- m1 := model.ChannelMember{
- ChannelId: c1.Id,
- UserId: u1.Id,
- NotifyProps: model.GetDefaultChannelNotifyProps(),
- }
- store.Must(ss.Channel().SaveMember(&m1))
-
- u1.DeleteAt = model.GetMillis()
- store.Must(ss.User().Update(u1, true))
-
- if result := <-ss.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil {
- t.Fatalf("failed to update extras by user: %v", result.Err)
- }
-
- u1.DeleteAt = 0
- store.Must(ss.User().Update(u1, true))
-
- if result := <-ss.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil {
- t.Fatalf("failed to update extras by user: %v", result.Err)
- }
-}
-
func testChannelStoreSearchMore(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
diff --git a/store/storetest/compliance_store.go b/store/storetest/compliance_store.go
index eb29bedc7..50a62531f 100644
--- a/store/storetest/compliance_store.go
+++ b/store/storetest/compliance_store.go
@@ -16,7 +16,10 @@ func TestComplianceStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testComplianceStore(t, ss) })
t.Run("ComplianceExport", func(t *testing.T) { testComplianceExport(t, ss) })
t.Run("ComplianceExportDirectMessages", func(t *testing.T) { testComplianceExportDirectMessages(t, ss) })
- t.Run("MessageExport", func(t *testing.T) { testComplianceMessageExport(t, ss) })
+ t.Run("MessageExportPublicChannel", func(t *testing.T) { testMessageExportPublicChannel(t, ss) })
+ t.Run("MessageExportPrivateChannel", func(t *testing.T) { testMessageExportPrivateChannel(t, ss) })
+ t.Run("MessageExportDirectMessageChannel", func(t *testing.T) { testMessageExportDirectMessageChannel(t, ss) })
+ t.Run("MessageExportGroupMessageChannel", func(t *testing.T) { testMessageExportGroupMessageChannel(t, ss) })
}
func testComplianceStore(t *testing.T, ss store.Store) {
@@ -319,7 +322,7 @@ func testComplianceExportDirectMessages(t *testing.T, ss store.Store) {
}
}
-func testComplianceMessageExport(t *testing.T, ss store.Store) {
+func testMessageExportPublicChannel(t *testing.T, ss store.Store) {
// get the starting number of message export entries
startTime := model.GetMillis()
var numMessageExports = 0
@@ -360,15 +363,14 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
UserId: user2.Id,
}, -1))
- // need a public channel as well as a DM channel between the two users
+ // need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
- DisplayName: "Channel2",
+ DisplayName: "Public Channel",
Type: model.CHANNEL_OPEN,
}
channel = store.Must(ss.Channel().Save(channel, -1)).(*model.Channel)
- directMessageChannel := store.Must(ss.Channel().CreateDirectChannel(user1.Id, user2.Id)).(*model.Channel)
// user1 posts twice in the public channel
post1 := &model.Post{
@@ -387,22 +389,114 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
}
post2 = store.Must(ss.Post().Save(post2)).(*model.Post)
- // user1 also sends a DM to user2
- post3 := &model.Post{
- ChannelId: directMessageChannel.Id,
+ // fetch the message exports for both posts that user1 sent
+ messageExportMap := map[string]model.MessageExport{}
+ if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ messages := r1.Data.([]*model.MessageExport)
+ assert.Equal(t, numMessageExports+2, len(messages))
+
+ for _, v := range messages {
+ messageExportMap[*v.PostId] = *v
+ }
+ }
+
+ // post1 was made by user1 in channel1 and team1
+ assert.Equal(t, post1.Id, *messageExportMap[post1.Id].PostId)
+ assert.Equal(t, post1.CreateAt, *messageExportMap[post1.Id].PostCreateAt)
+ assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage)
+ assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId)
+ assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
+ assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
+ assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
+ assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
+
+ // post2 was made by user1 in channel1 and team1
+ assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId)
+ assert.Equal(t, post2.CreateAt, *messageExportMap[post2.Id].PostCreateAt)
+ assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage)
+ assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId)
+ assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
+ assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
+ assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
+ assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
+}
+
+func testMessageExportPrivateChannel(t *testing.T, ss store.Store) {
+ // get the starting number of message export entries
+ startTime := model.GetMillis()
+ var numMessageExports = 0
+ if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ messages := r1.Data.([]*model.MessageExport)
+ numMessageExports = len(messages)
+ }
+
+ // need a team
+ team := &model.Team{
+ DisplayName: "DisplayName",
+ Name: "zz" + model.NewId() + "b",
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ }
+ team = store.Must(ss.Team().Save(team)).(*model.Team)
+
+ // and two users that are a part of that team
+ user1 := &model.User{
+ Email: model.NewId(),
+ Username: model.NewId(),
+ }
+ user1 = store.Must(ss.User().Save(user1)).(*model.User)
+ store.Must(ss.Team().SaveMember(&model.TeamMember{
+ TeamId: team.Id,
+ UserId: user1.Id,
+ }, -1))
+
+ user2 := &model.User{
+ Email: model.NewId(),
+ Username: model.NewId(),
+ }
+ user2 = store.Must(ss.User().Save(user2)).(*model.User)
+ store.Must(ss.Team().SaveMember(&model.TeamMember{
+ TeamId: team.Id,
+ UserId: user2.Id,
+ }, -1))
+
+ // need a private channel
+ channel := &model.Channel{
+ TeamId: team.Id,
+ Name: model.NewId(),
+ DisplayName: "Private Channel",
+ Type: model.CHANNEL_PRIVATE,
+ }
+ channel = store.Must(ss.Channel().Save(channel, -1)).(*model.Channel)
+
+ // user1 posts twice in the private channel
+ post1 := &model.Post{
+ ChannelId: channel.Id,
UserId: user1.Id,
- CreateAt: startTime + 20,
- Message: "zz" + model.NewId() + "c",
+ CreateAt: startTime,
+ Message: "zz" + model.NewId() + "a",
+ }
+ post1 = store.Must(ss.Post().Save(post1)).(*model.Post)
+
+ post2 := &model.Post{
+ ChannelId: channel.Id,
+ UserId: user1.Id,
+ CreateAt: startTime + 10,
+ Message: "zz" + model.NewId() + "b",
}
- post3 = store.Must(ss.Post().Save(post3)).(*model.Post)
+ post2 = store.Must(ss.Post().Save(post2)).(*model.Post)
- // fetch the message exports for all three posts that user1 sent
+ // fetch the message exports for both posts that user1 sent
messageExportMap := map[string]model.MessageExport{}
if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil {
t.Fatal(r1.Err)
} else {
messages := r1.Data.([]*model.MessageExport)
- assert.Equal(t, numMessageExports+3, len(messages))
+ assert.Equal(t, numMessageExports+2, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
@@ -415,6 +509,7 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
+ assert.Equal(t, channel.Type, *messageExportMap[post1.Id].ChannelType)
assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
@@ -425,16 +520,179 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
+ assert.Equal(t, channel.Type, *messageExportMap[post2.Id].ChannelType)
assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
+}
+
+func testMessageExportDirectMessageChannel(t *testing.T, ss store.Store) {
+ // get the starting number of message export entries
+ startTime := model.GetMillis()
+ var numMessageExports = 0
+ if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ messages := r1.Data.([]*model.MessageExport)
+ numMessageExports = len(messages)
+ }
+
+ // need a team
+ team := &model.Team{
+ DisplayName: "DisplayName",
+ Name: "zz" + model.NewId() + "b",
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ }
+ team = store.Must(ss.Team().Save(team)).(*model.Team)
+
+ // and two users that are a part of that team
+ user1 := &model.User{
+ Email: model.NewId(),
+ Username: model.NewId(),
+ }
+ user1 = store.Must(ss.User().Save(user1)).(*model.User)
+ store.Must(ss.Team().SaveMember(&model.TeamMember{
+ TeamId: team.Id,
+ UserId: user1.Id,
+ }, -1))
+
+ user2 := &model.User{
+ Email: model.NewId(),
+ Username: model.NewId(),
+ }
+ user2 = store.Must(ss.User().Save(user2)).(*model.User)
+ store.Must(ss.Team().SaveMember(&model.TeamMember{
+ TeamId: team.Id,
+ UserId: user2.Id,
+ }, -1))
+
+ // as well as a DM channel between those users
+ directMessageChannel := store.Must(ss.Channel().CreateDirectChannel(user1.Id, user2.Id)).(*model.Channel)
+
+ // user1 also sends a DM to user2
+ post := &model.Post{
+ ChannelId: directMessageChannel.Id,
+ UserId: user1.Id,
+ CreateAt: startTime + 20,
+ Message: "zz" + model.NewId() + "c",
+ }
+ post = store.Must(ss.Post().Save(post)).(*model.Post)
+
+ // fetch the message export for the post that user1 sent
+ messageExportMap := map[string]model.MessageExport{}
+ if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ messages := r1.Data.([]*model.MessageExport)
+ assert.Equal(t, numMessageExports+1, len(messages))
+
+ for _, v := range messages {
+ messageExportMap[*v.PostId] = *v
+ }
+ }
+
+ // post is a DM between user1 and user2
+ // there is no channel display name for direct messages, so we sub in the string "Direct Message" instead
+ assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId)
+ assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt)
+ assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage)
+ assert.Equal(t, directMessageChannel.Id, *messageExportMap[post.Id].ChannelId)
+ assert.Equal(t, "Direct Message", *messageExportMap[post.Id].ChannelDisplayName)
+ assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId)
+ assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail)
+ assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username)
+}
+
+func testMessageExportGroupMessageChannel(t *testing.T, ss store.Store) {
+ // get the starting number of message export entries
+ startTime := model.GetMillis()
+ var numMessageExports = 0
+ if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ messages := r1.Data.([]*model.MessageExport)
+ numMessageExports = len(messages)
+ }
+
+ // need a team
+ team := &model.Team{
+ DisplayName: "DisplayName",
+ Name: "zz" + model.NewId() + "b",
+ Email: model.NewId() + "@nowhere.com",
+ Type: model.TEAM_OPEN,
+ }
+ team = store.Must(ss.Team().Save(team)).(*model.Team)
+
+ // and three users that are a part of that team
+ user1 := &model.User{
+ Email: model.NewId(),
+ Username: model.NewId(),
+ }
+ user1 = store.Must(ss.User().Save(user1)).(*model.User)
+ store.Must(ss.Team().SaveMember(&model.TeamMember{
+ TeamId: team.Id,
+ UserId: user1.Id,
+ }, -1))
+
+ user2 := &model.User{
+ Email: model.NewId(),
+ Username: model.NewId(),
+ }
+ user2 = store.Must(ss.User().Save(user2)).(*model.User)
+ store.Must(ss.Team().SaveMember(&model.TeamMember{
+ TeamId: team.Id,
+ UserId: user2.Id,
+ }, -1))
+
+ user3 := &model.User{
+ Email: model.NewId(),
+ Username: model.NewId(),
+ }
+ user3 = store.Must(ss.User().Save(user3)).(*model.User)
+ store.Must(ss.Team().SaveMember(&model.TeamMember{
+ TeamId: team.Id,
+ UserId: user3.Id,
+ }, -1))
+
+ // can't create a group channel directly, because importing app creates an import cycle, so we have to fake it
+ groupMessageChannel := &model.Channel{
+ TeamId: team.Id,
+ Name: model.NewId(),
+ Type: model.CHANNEL_GROUP,
+ }
+ groupMessageChannel = store.Must(ss.Channel().Save(groupMessageChannel, -1)).(*model.Channel)
+
+ // user1 posts in the GM
+ post := &model.Post{
+ ChannelId: groupMessageChannel.Id,
+ UserId: user1.Id,
+ CreateAt: startTime + 20,
+ Message: "zz" + model.NewId() + "c",
+ }
+ post = store.Must(ss.Post().Save(post)).(*model.Post)
+
+ // fetch the message export for the post that user1 sent
+ messageExportMap := map[string]model.MessageExport{}
+ if r1 := <-ss.Compliance().MessageExport(startTime-10, 10); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ messages := r1.Data.([]*model.MessageExport)
+ assert.Equal(t, numMessageExports+1, len(messages))
+
+ for _, v := range messages {
+ messageExportMap[*v.PostId] = *v
+ }
+ }
- // post3 is a DM between user1 and user2
- assert.Equal(t, post3.Id, *messageExportMap[post3.Id].PostId)
- assert.Equal(t, post3.CreateAt, *messageExportMap[post3.Id].PostCreateAt)
- assert.Equal(t, post3.Message, *messageExportMap[post3.Id].PostMessage)
- assert.Equal(t, directMessageChannel.Id, *messageExportMap[post3.Id].ChannelId)
- assert.Equal(t, user1.Id, *messageExportMap[post3.Id].UserId)
- assert.Equal(t, user1.Email, *messageExportMap[post3.Id].UserEmail)
- assert.Equal(t, user1.Username, *messageExportMap[post3.Id].Username)
+ // post is a DM between user1 and user2
+ // there is no channel display name for direct messages, so we sub in the string "Direct Message" instead
+ assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId)
+ assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt)
+ assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage)
+ assert.Equal(t, groupMessageChannel.Id, *messageExportMap[post.Id].ChannelId)
+ assert.Equal(t, "Group Message", *messageExportMap[post.Id].ChannelDisplayName)
+ assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId)
+ assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail)
+ assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username)
}
diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go
index 5379c2fb4..6eab47073 100644
--- a/store/storetest/mocks/ChannelStore.go
+++ b/store/storetest/mocks/ChannelStore.go
@@ -61,6 +61,11 @@ func (_m *ChannelStore) AutocompleteInTeam(teamId string, term string) store.Sto
return r0
}
+// ClearCaches provides a mock function with given fields:
+func (_m *ChannelStore) ClearCaches() {
+ _m.Called()
+}
+
// CreateDirectChannel provides a mock function with given fields: userId, otherUserId
func (_m *ChannelStore) CreateDirectChannel(userId string, otherUserId string) store.StoreChannel {
ret := _m.Called(userId, otherUserId)
@@ -93,22 +98,6 @@ func (_m *ChannelStore) Delete(channelId string, time int64) store.StoreChannel
return r0
}
-// ExtraUpdateByUser provides a mock function with given fields: userId, time
-func (_m *ChannelStore) ExtraUpdateByUser(userId string, time int64) store.StoreChannel {
- ret := _m.Called(userId, time)
-
- var r0 store.StoreChannel
- if rf, ok := ret.Get(0).(func(string, int64) store.StoreChannel); ok {
- r0 = rf(userId, time)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).(store.StoreChannel)
- }
- }
-
- return r0
-}
-
// Get provides a mock function with given fields: id, allowFromCache
func (_m *ChannelStore) Get(id string, allowFromCache bool) store.StoreChannel {
ret := _m.Called(id, allowFromCache)
diff --git a/store/storetest/mocks/FileInfoStore.go b/store/storetest/mocks/FileInfoStore.go
index 9b479ff3a..4dddf0bd7 100644
--- a/store/storetest/mocks/FileInfoStore.go
+++ b/store/storetest/mocks/FileInfoStore.go
@@ -29,6 +29,11 @@ func (_m *FileInfoStore) AttachToPost(fileId string, postId string) store.StoreC
return r0
}
+// ClearCaches provides a mock function with given fields:
+func (_m *FileInfoStore) ClearCaches() {
+ _m.Called()
+}
+
// DeleteForPost provides a mock function with given fields: postId
func (_m *FileInfoStore) DeleteForPost(postId string) store.StoreChannel {
ret := _m.Called(postId)
diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go
index 05e3bde34..c405d5030 100644
--- a/store/storetest/mocks/PostStore.go
+++ b/store/storetest/mocks/PostStore.go
@@ -61,6 +61,11 @@ func (_m *PostStore) AnalyticsUserCountsWithPostsByDay(teamId string) store.Stor
return r0
}
+// ClearCaches provides a mock function with given fields:
+func (_m *PostStore) ClearCaches() {
+ _m.Called()
+}
+
// Delete provides a mock function with given fields: postId, time
func (_m *PostStore) Delete(postId string, time int64) store.StoreChannel {
ret := _m.Called(postId, time)
diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go
index 8a7f030dc..d38fb5f27 100644
--- a/store/storetest/mocks/TeamStore.go
+++ b/store/storetest/mocks/TeamStore.go
@@ -461,13 +461,13 @@ func (_m *TeamStore) UpdateDisplayName(name string, teamId string) store.StoreCh
return r0
}
-// UpdateMember provides a mock function with given fields: member
-func (_m *TeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel {
- ret := _m.Called(member)
+// UpdateLastTeamIconUpdate provides a mock function with given fields: teamId, curTime
+func (_m *TeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) store.StoreChannel {
+ ret := _m.Called(teamId, curTime)
var r0 store.StoreChannel
- if rf, ok := ret.Get(0).(func(*model.TeamMember) store.StoreChannel); ok {
- r0 = rf(member)
+ 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)
@@ -477,13 +477,13 @@ 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)
+// UpdateMember provides a mock function with given fields: member
+func (_m *TeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel {
+ ret := _m.Called(member)
var r0 store.StoreChannel
- if rf, ok := ret.Get(0).(func(string, int64) store.StoreChannel); ok {
- r0 = rf(teamId, curTime)
+ if rf, ok := ret.Get(0).(func(*model.TeamMember) store.StoreChannel); ok {
+ r0 = rf(member)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go
index 7d1fd8c38..369a29e7a 100644
--- a/store/storetest/mocks/UserStore.go
+++ b/store/storetest/mocks/UserStore.go
@@ -77,6 +77,11 @@ func (_m *UserStore) AnalyticsUniqueUserCount(teamId string) store.StoreChannel
return r0
}
+// ClearCaches provides a mock function with given fields:
+func (_m *UserStore) ClearCaches() {
+ _m.Called()
+}
+
// Get provides a mock function with given fields: id
func (_m *UserStore) Get(id string) store.StoreChannel {
ret := _m.Called(id)
@@ -349,6 +354,22 @@ func (_m *UserStore) GetProfilesInChannel(channelId string, offset int, limit in
return r0
}
+// GetProfilesInChannelByStatus provides a mock function with given fields: channelId, offset, limit
+func (_m *UserStore) GetProfilesInChannelByStatus(channelId string, offset int, limit int) store.StoreChannel {
+ ret := _m.Called(channelId, offset, limit)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok {
+ r0 = rf(channelId, offset, limit)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
+
// GetProfilesNotInChannel provides a mock function with given fields: teamId, channelId, offset, limit
func (_m *UserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) store.StoreChannel {
ret := _m.Called(teamId, channelId, offset, limit)
diff --git a/store/storetest/mocks/WebhookStore.go b/store/storetest/mocks/WebhookStore.go
index aa66e0600..bf5b636eb 100644
--- a/store/storetest/mocks/WebhookStore.go
+++ b/store/storetest/mocks/WebhookStore.go
@@ -45,6 +45,11 @@ func (_m *WebhookStore) AnalyticsOutgoingCount(teamId string) store.StoreChannel
return r0
}
+// ClearCaches provides a mock function with given fields:
+func (_m *WebhookStore) ClearCaches() {
+ _m.Called()
+}
+
// DeleteIncoming provides a mock function with given fields: webhookId, time
func (_m *WebhookStore) DeleteIncoming(webhookId string, time int64) store.StoreChannel {
ret := _m.Called(webhookId, time)
diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go
index 47f04d1bb..2fd7d4190 100644
--- a/store/storetest/user_store.go
+++ b/store/storetest/user_store.go
@@ -25,6 +25,7 @@ func TestUserStore(t *testing.T, ss store.Store) {
t.Run("GetAllProfiles", func(t *testing.T) { testUserStoreGetAllProfiles(t, ss) })
t.Run("GetProfiles", func(t *testing.T) { testUserStoreGetProfiles(t, ss) })
t.Run("GetProfilesInChannel", func(t *testing.T) { testUserStoreGetProfilesInChannel(t, ss) })
+ t.Run("GetProfilesInChannelByStatus", func(t *testing.T) { testUserStoreGetProfilesInChannelByStatus(t, ss) })
t.Run("GetProfilesWithoutTeam", func(t *testing.T) { testUserStoreGetProfilesWithoutTeam(t, ss) })
t.Run("GetAllProfilesInChannel", func(t *testing.T) { testUserStoreGetAllProfilesInChannel(t, ss) })
t.Run("GetProfilesNotInChannel", func(t *testing.T) { testUserStoreGetProfilesNotInChannel(t, ss) })
@@ -464,6 +465,82 @@ func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) {
}
}
+func testUserStoreGetProfilesInChannelByStatus(t *testing.T, ss store.Store) {
+ teamId := model.NewId()
+
+ u1 := &model.User{}
+ u1.Email = model.NewId()
+ store.Must(ss.User().Save(u1))
+ store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1))
+
+ u2 := &model.User{}
+ u2.Email = model.NewId()
+ store.Must(ss.User().Save(u2))
+ store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1))
+
+ c1 := model.Channel{}
+ c1.TeamId = teamId
+ c1.DisplayName = "Profiles in channel"
+ c1.Name = "profiles-" + model.NewId()
+ c1.Type = model.CHANNEL_OPEN
+
+ c2 := model.Channel{}
+ c2.TeamId = teamId
+ c2.DisplayName = "Profiles in private"
+ c2.Name = "profiles-" + model.NewId()
+ c2.Type = model.CHANNEL_PRIVATE
+
+ store.Must(ss.Channel().Save(&c1, -1))
+ store.Must(ss.Channel().Save(&c2, -1))
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = c1.Id
+ m1.UserId = u1.Id
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ m2 := model.ChannelMember{}
+ m2.ChannelId = c1.Id
+ m2.UserId = u2.Id
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ m3 := model.ChannelMember{}
+ m3.ChannelId = c2.Id
+ m3.UserId = u1.Id
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ store.Must(ss.Channel().SaveMember(&m1))
+ store.Must(ss.Channel().SaveMember(&m2))
+ store.Must(ss.Channel().SaveMember(&m3))
+
+ if r1 := <-ss.User().GetProfilesInChannelByStatus(c1.Id, 0, 100); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.([]*model.User)
+ if len(users) != 2 {
+ t.Fatal("invalid returned users")
+ }
+
+ found := false
+ for _, u := range users {
+ if u.Id == u1.Id {
+ found = true
+ }
+ }
+
+ if !found {
+ t.Fatal("missing user")
+ }
+ }
+
+ if r2 := <-ss.User().GetProfilesInChannelByStatus(c2.Id, 0, 1); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if len(r2.Data.([]*model.User)) != 1 {
+ t.Fatal("should have returned only 1 user")
+ }
+ }
+}
+
func testUserStoreGetProfilesWithoutTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
diff --git a/templates/unsupported_browser.html b/templates/unsupported_browser.html
index 79656e10c..f5c515230 100644
--- a/templates/unsupported_browser.html
+++ b/templates/unsupported_browser.html
@@ -151,7 +151,7 @@
</li>
<li>
<a href="https://www.mozilla.org/en-US/firefox/new/" target="_blank" rel="noopener noreferrer">
- <span>Mozzilla Firefox 52+</span>
+ <span>Mozilla Firefox 52+</span>
</a>
</li>
<li>
diff --git a/utils/config.go b/utils/config.go
index b28cf918d..8e9bafc6e 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/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/mail.go b/utils/mail.go
index 2bc0ce9e1..3b9f4bd9d 100644
--- a/utils/mail.go
+++ b/utils/mail.go
@@ -186,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/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/vendor/github.com/mssola/user_agent/.travis.yml b/vendor/github.com/mssola/user_agent/.travis.yml
deleted file mode 100644
index 96f43d112..000000000
--- a/vendor/github.com/mssola/user_agent/.travis.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-language: go
-go:
- - 1.4.x
- - 1.5.x
- - 1.6.x
- - 1.7.x
- - 1.8.x
- - 1.9.x
- - 1.x
- - tip
-matrix:
- allow_failures:
- - go: tip
diff --git a/vendor/github.com/mssola/user_agent/LICENSE b/vendor/github.com/mssola/user_agent/LICENSE
deleted file mode 100644
index 1c42691ce..000000000
--- a/vendor/github.com/mssola/user_agent/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2012-2018 Miquel Sabaté Solà
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/github.com/mssola/user_agent/README.md b/vendor/github.com/mssola/user_agent/README.md
deleted file mode 100644
index 60427ec53..000000000
--- a/vendor/github.com/mssola/user_agent/README.md
+++ /dev/null
@@ -1,51 +0,0 @@
-
-# UserAgent [![Build Status](https://travis-ci.org/mssola/user_agent.png?branch=master)](https://travis-ci.org/mssola/user_agent) [![GoDoc](https://godoc.org/github.com/mssola/user_agent?status.png)](http://godoc.org/github.com/mssola/user_agent)
-
-
-UserAgent is a Go library that parses HTTP User Agents.
-
-## Usage
-
-~~~ go
-package main
-
-import (
- "fmt"
-
- "github.com/mssola/user_agent"
-)
-
-func main() {
- // The "New" function will create a new UserAgent object and it will parse
- // the given string. If you need to parse more strings, you can re-use
- // this object and call: ua.Parse("another string")
- ua := user_agent.New("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11")
-
- fmt.Printf("%v\n", ua.Mobile()) // => false
- fmt.Printf("%v\n", ua.Bot()) // => false
- fmt.Printf("%v\n", ua.Mozilla()) // => "5.0"
-
- fmt.Printf("%v\n", ua.Platform()) // => "X11"
- fmt.Printf("%v\n", ua.OS()) // => "Linux x86_64"
-
- name, version := ua.Engine()
- fmt.Printf("%v\n", name) // => "AppleWebKit"
- fmt.Printf("%v\n", version) // => "537.11"
-
- name, version = ua.Browser()
- fmt.Printf("%v\n", name) // => "Chrome"
- fmt.Printf("%v\n", version) // => "23.0.1271.97"
-
- // Let's see an example with a bot.
-
- ua.Parse("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
-
- fmt.Printf("%v\n", ua.Bot()) // => true
-
- name, version = ua.Browser()
- fmt.Printf("%v\n", name) // => Googlebot
- fmt.Printf("%v\n", version) // => 2.1
-}
-~~~
-
-Copyright &copy; 2012-2018 Miquel Sabaté Solà, released under the MIT License.
diff --git a/vendor/github.com/mssola/user_agent/all_test.go b/vendor/github.com/mssola/user_agent/all_test.go
deleted file mode 100644
index a5d5ee648..000000000
--- a/vendor/github.com/mssola/user_agent/all_test.go
+++ /dev/null
@@ -1,594 +0,0 @@
-// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com>
-// This file is licensed under the MIT license.
-// See the LICENSE file.
-
-package user_agent
-
-import (
- "fmt"
- "reflect"
- "testing"
-)
-
-// Slice that contains all the tests. Each test is contained in a struct
-// that groups the title of the test, the User-Agent string to be tested and the expected value.
-var uastrings = []struct {
- title string
- ua string
- expected string
- expectedOS *OSInfo
-}{
- // Bots
- {
- title: "GoogleBot",
- ua: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
- expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:false",
- },
- {
- title: "GoogleBotSmartphone (iPhone)",
- ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
- expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:true",
- },
- {
- title: "GoogleBotSmartphone (Android)",
- ua: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
- expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:true",
- },
- {
- title: "BingBot",
- ua: "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
- expected: "Mozilla:5.0 Browser:bingbot-2.0 Bot:true Mobile:false",
- },
- {
- title: "BaiduBot",
- ua: "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)",
- expected: "Mozilla:5.0 Browser:Baiduspider-2.0 Bot:true Mobile:false",
- },
- {
- title: "Twitterbot",
- ua: "Twitterbot",
- expected: "Browser:Twitterbot Bot:true Mobile:false",
- },
- {
- title: "YahooBot",
- ua: "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)",
- expected: "Mozilla:5.0 Browser:Yahoo! Slurp Bot:true Mobile:false",
- },
- {
- title: "FacebookExternalHit",
- ua: "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)",
- expected: "Browser:facebookexternalhit-1.1 Bot:true Mobile:false",
- },
- {
- title: "FacebookPlatform",
- ua: "facebookplatform/1.0 (+http://developers.facebook.com)",
- expected: "Browser:facebookplatform-1.0 Bot:true Mobile:false",
- },
- {
- title: "FaceBot",
- ua: "Facebot",
- expected: "Browser:Facebot Bot:true Mobile:false",
- },
- {
- title: "NutchCVS",
- ua: "NutchCVS/0.8-dev (Nutch; http://lucene.apache.org/nutch/bot.html; nutch-agent@lucene.apache.org)",
- expected: "Browser:NutchCVS Bot:true Mobile:false",
- },
- {
- title: "MJ12bot",
- ua: "Mozilla/5.0 (compatible; MJ12bot/v1.2.4; http://www.majestic12.co.uk/bot.php?+)",
- expected: "Mozilla:5.0 Browser:MJ12bot-v1.2.4 Bot:true Mobile:false",
- },
- {
- title: "MJ12bot",
- ua: "MJ12bot/v1.0.8 (http://majestic12.co.uk/bot.php?+)",
- expected: "Browser:MJ12bot Bot:true Mobile:false",
- },
- {
- title: "AhrefsBot",
- ua: "Mozilla/5.0 (compatible; AhrefsBot/4.0; +http://ahrefs.com/robot/)",
- expected: "Mozilla:5.0 Browser:AhrefsBot-4.0 Bot:true Mobile:false",
- },
-
- // Internet Explorer
- {
- title: "IE10",
- ua: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows 8", "Windows", "8"},
- },
- {
- title: "Tablet",
- ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; ARM; Trident/6.0; Touch; .NET4.0E; .NET4.0C; Tablet PC 2.0)",
- expected: "Mozilla:4.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false",
- },
- {
- title: "Touch",
- ua: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; ARM; Trident/6.0; Touch)",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false",
- },
- {
- title: "Phone",
- ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0; SAMSUNG; SGH-i917)",
- expected: "Mozilla:4.0 Platform:Windows OS:Windows Phone OS 7.0 Browser:Internet Explorer-7.0 Engine:Trident Bot:false Mobile:true",
- expectedOS: &OSInfo{"Windows Phone OS 7.0", "Windows Phone OS", "7.0"},
- },
- {
- title: "IE6",
- ua: "Mozilla/4.0 (compatible; MSIE6.0; Windows NT 5.0; .NET CLR 1.1.4322)",
- expected: "Mozilla:4.0 Platform:Windows OS:Windows 2000 Browser:Internet Explorer-6.0 Engine:Trident Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows 2000", "Windows", "2000"},
- },
- {
- title: "IE8Compatibility",
- ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; MS-RTC LM 8)",
- expected: "Mozilla:4.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-8.0 Engine:Trident Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows 7", "Windows", "7"},
- },
- {
- title: "IE10Compatibility",
- ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; MS-RTC LM 8)",
- expected: "Mozilla:4.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false",
- },
- {
- title: "IE11Win81",
- ua: "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 8.1 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows 8.1", "Windows", "8.1"},
- },
- {
- title: "IE11Win7",
- ua: "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false",
- },
- {
- title: "IE11b32Win7b64",
- ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false",
- },
- {
- title: "IE11b32Win7b64MDDRJS",
- ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDRJS; rv:11.0) like Gecko",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false",
- },
- {
- title: "IE11Compatibility",
- ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0)",
- expected: "Mozilla:4.0 Platform:Windows OS:Windows 8.1 Browser:Internet Explorer-7.0 Engine:Trident Bot:false Mobile:false",
- },
-
- // Microsoft Edge
- {
- title: "EdgeDesktop",
- ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 10 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows 10", "Windows", "10"},
- },
- {
- title: "EdgeMobile",
- ua: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; DEVICE INFO) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Mobile Safari/537.36 Edge/12.10240",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows Phone 10.0 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:true",
- },
-
- // Gecko
- {
- title: "FirefoxMac",
- ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8",
- expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Browser:Firefox-4.0b8 Engine:Gecko-20100101 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Intel Mac OS X 10.6", "Mac OS X", "10.6"},
- },
- {
- title: "FirefoxMacLoc",
- ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13",
- expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Localization:en-US Browser:Firefox-3.6.13 Engine:Gecko-20101203 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Intel Mac OS X 10.6", "Mac OS X", "10.6"},
- },
- {
- title: "FirefoxLinux",
- ua: "Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Firefox-17.0 Engine:Gecko-20100101 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Linux x86_64", "Linux", ""},
- },
- {
- title: "FirefoxLinux - Ubuntu V50",
- ua: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0",
- expected: "Mozilla:5.0 Platform:X11 OS:Ubuntu Browser:Firefox-50.0 Engine:Gecko-20100101 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Ubuntu", "Ubuntu", ""},
- },
- {
- title: "FirefoxWin",
- ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.14",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en-US Browser:Firefox-2.0.0.14 Engine:Gecko-20080404 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows XP", "Windows", "XP"},
- },
- {
- title: "Firefox29Win7",
- ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Firefox-29.0 Engine:Gecko-20100101 Bot:false Mobile:false",
- },
- {
- title: "CaminoMac",
- ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.14) Gecko/20080409 Camino/1.6 (like Firefox/2.0.0.14)",
- expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X Localization:en Browser:Camino-1.6 Engine:Gecko-20080409 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Intel Mac OS X", "Mac OS X", ""},
- },
- {
- title: "Iceweasel",
- ua: "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061024 Iceweasel/2.0 (Debian-2.0+dfsg-1)",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux i686 Localization:en-US Browser:Iceweasel-2.0 Engine:Gecko-20061024 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Linux i686", "Linux", ""},
- },
- {
- title: "SeaMonkey",
- ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.4) Gecko/20091017 SeaMonkey/2.0",
- expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Localization:en-US Browser:SeaMonkey-2.0 Engine:Gecko-20091017 Bot:false Mobile:false",
- },
- {
- title: "AndroidFirefox",
- ua: "Mozilla/5.0 (Android; Mobile; rv:17.0) Gecko/17.0 Firefox/17.0",
- expected: "Mozilla:5.0 Platform:Mobile OS:Android Browser:Firefox-17.0 Engine:Gecko-17.0 Bot:false Mobile:true",
- },
- {
- title: "AndroidFirefoxTablet",
- ua: "Mozilla/5.0 (Android; Tablet; rv:26.0) Gecko/26.0 Firefox/26.0",
- expected: "Mozilla:5.0 Platform:Tablet OS:Android Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true",
- expectedOS: &OSInfo{"Android", "Android", ""},
- },
- {
- title: "FirefoxOS",
- ua: "Mozilla/5.0 (Mobile; rv:26.0) Gecko/26.0 Firefox/26.0",
- expected: "Mozilla:5.0 Platform:Mobile OS:FirefoxOS Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true",
- expectedOS: &OSInfo{"FirefoxOS", "FirefoxOS", ""},
- },
- {
- title: "FirefoxOSTablet",
- ua: "Mozilla/5.0 (Tablet; rv:26.0) Gecko/26.0 Firefox/26.0",
- expected: "Mozilla:5.0 Platform:Tablet OS:FirefoxOS Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true",
- },
- {
- title: "FirefoxWinXP",
- ua: "Mozilla/5.0 (Windows NT 5.2; rv:31.0) Gecko/20100101 Firefox/31.0",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows XP x64 Edition Browser:Firefox-31.0 Engine:Gecko-20100101 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows XP x64 Edition", "Windows", "XP"},
- },
- {
- title: "FirefoxMRA",
- ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:24.0) Gecko/20130405 MRA 5.5 (build 02842) Firefox/24.0 (.NET CLR 3.5.30729)",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en-US Browser:Firefox-24.0 Engine:Gecko-20130405 Bot:false Mobile:false",
- },
-
- // Opera
- {
- title: "OperaMac",
- ua: "Opera/9.27 (Macintosh; Intel Mac OS X; U; en)",
- expected: "Platform:Macintosh OS:Intel Mac OS X Localization:en Browser:Opera-9.27 Engine:Presto Bot:false Mobile:false",
- expectedOS: &OSInfo{"Intel Mac OS X", "Mac OS X", ""},
- },
- {
- title: "OperaWin",
- ua: "Opera/9.27 (Windows NT 5.1; U; en)",
- expected: "Platform:Windows OS:Windows XP Localization:en Browser:Opera-9.27 Engine:Presto Bot:false Mobile:false",
- },
- {
- title: "OperaWinNoLocale",
- ua: "Opera/9.80 (Windows NT 5.1) Presto/2.12.388 Version/12.10",
- expected: "Platform:Windows OS:Windows XP Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false",
- },
- {
- title: "OperaWin2Comment",
- ua: "Opera/9.80 (Windows NT 6.0; WOW64) Presto/2.12.388 Version/12.15",
- expected: "Platform:Windows OS:Windows Vista Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Windows Vista", "Windows", "Vista"},
- },
- {
- title: "OperaMinimal",
- ua: "Opera/9.80",
- expected: "Browser:Opera-9.80 Engine:Presto Bot:false Mobile:false",
- },
- {
- title: "OperaFull",
- ua: "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10",
- expected: "Platform:Windows OS:Windows Vista Localization:en Browser:Opera-9.80 Engine:Presto-2.2.15 Bot:false Mobile:false",
- },
- {
- title: "OperaLinux",
- ua: "Opera/9.80 (X11; Linux x86_64) Presto/2.12.388 Version/12.10",
- expected: "Platform:X11 OS:Linux x86_64 Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false",
- },
- {
- title: "OperaLinux - Ubuntu V41",
- ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36 OPR/41.0.2353.69",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Opera-41.0.2353.69 Engine:AppleWebKit-537.36 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Linux x86_64", "Linux", ""},
- },
- {
- title: "OperaAndroid",
- ua: "Opera/9.80 (Android 4.2.1; Linux; Opera Mobi/ADR-1212030829) Presto/2.11.355 Version/12.10",
- expected: "Platform:Android 4.2.1 OS:Linux Browser:Opera-9.80 Engine:Presto-2.11.355 Bot:false Mobile:true",
- expectedOS: &OSInfo{"Linux", "Linux", ""},
- },
- {
- title: "OperaNested",
- ua: "Opera/9.80 (Windows NT 5.1; MRA 6.0 (build 5831)) Presto/2.12.388 Version/12.10",
- expected: "Platform:Windows OS:Windows XP Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false",
- },
- {
- title: "OperaMRA",
- ua: "Opera/9.80 (Windows NT 6.1; U; MRA 5.8 (build 4139); en) Presto/2.9.168 Version/11.50",
- expected: "Platform:Windows OS:Windows 7 Localization:en Browser:Opera-9.80 Engine:Presto-2.9.168 Bot:false Mobile:false",
- },
-
- // Other
- {
- title: "Empty",
- ua: "",
- expected: "Bot:false Mobile:false",
- },
- {
- title: "Nil",
- ua: "nil",
- expected: "Browser:nil Bot:false Mobile:false",
- },
- {
- title: "Compatible",
- ua: "Mozilla/4.0 (compatible)",
- expected: "Browser:Mozilla-4.0 Bot:false Mobile:false",
- },
- {
- title: "Mozilla",
- ua: "Mozilla/5.0",
- expected: "Browser:Mozilla-5.0 Bot:false Mobile:false",
- },
- {
- title: "Amaya",
- ua: "amaya/9.51 libwww/5.4.0",
- expected: "Browser:amaya-9.51 Engine:libwww-5.4.0 Bot:false Mobile:false",
- },
- {
- title: "Rails",
- ua: "Rails Testing",
- expected: "Browser:Rails Engine:Testing Bot:false Mobile:false",
- },
- {
- title: "Python",
- ua: "Python-urllib/2.7",
- expected: "Browser:Python-urllib-2.7 Bot:false Mobile:false",
- },
- {
- title: "Curl",
- ua: "curl/7.28.1",
- expected: "Browser:curl-7.28.1 Bot:false Mobile:false",
- },
-
- // WebKit
- {
- title: "ChromeLinux",
- ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chrome-23.0.1271.97 Engine:AppleWebKit-537.11 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Linux x86_64", "Linux", ""},
- },
- {
- title: "ChromeLinux - Ubuntu V55",
- ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chrome-55.0.2883.75 Engine:AppleWebKit-537.36 Bot:false Mobile:false",
- },
- {
- title: "ChromeWin7",
- ua: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.168 Safari/535.19",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Chrome-18.0.1025.168 Engine:AppleWebKit-535.19 Bot:false Mobile:false",
- },
- {
- title: "ChromeMinimal",
- ua: "Mozilla/5.0 AppleWebKit/534.10 Chrome/8.0.552.215 Safari/534.10",
- expected: "Mozilla:5.0 Browser:Chrome-8.0.552.215 Engine:AppleWebKit-534.10 Bot:false Mobile:false",
- },
- {
- title: "ChromeMac",
- ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.231 Safari/534.10",
- expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10_6_5 Localization:en-US Browser:Chrome-8.0.552.231 Engine:AppleWebKit-534.10 Bot:false Mobile:false",
- expectedOS: &OSInfo{"Intel Mac OS X 10_6_5", "Mac OS X", "10.6.5"},
- },
- {
- title: "SafariMac",
- ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16",
- expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10_6_3 Localization:en-us Browser:Safari-5.0 Engine:AppleWebKit-533.16 Bot:false Mobile:false",
- },
- {
- title: "SafariWin",
- ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8",
- expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en Browser:Safari-4.0dp1 Engine:AppleWebKit-526.9 Bot:false Mobile:false",
- },
- {
- title: "iPhone7",
- ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53",
- expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 7_0_3 like Mac OS X Browser:Safari-7.0 Engine:AppleWebKit-537.51.1 Bot:false Mobile:true",
- expectedOS: &OSInfo{"CPU iPhone OS 7_0_3 like Mac OS X", "iPhone OS", "7.0.3"},
- },
- {
- title: "iPhone",
- ua: "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/419",
- expected: "Mozilla:5.0 Platform:iPhone OS:CPU like Mac OS X Localization:en Browser:Safari-3.0 Engine:AppleWebKit-420.1 Bot:false Mobile:true",
- },
- {
- title: "iPod",
- ua: "Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/419",
- expected: "Mozilla:5.0 Platform:iPod OS:CPU like Mac OS X Localization:en Browser:Safari-3.0 Engine:AppleWebKit-420.1 Bot:false Mobile:true",
- },
- {
- title: "iPad",
- ua: "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10",
- expected: "Mozilla:5.0 Platform:iPad OS:CPU OS 3_2 like Mac OS X Localization:en-us Browser:Safari-4.0.4 Engine:AppleWebKit-531.21.10 Bot:false Mobile:true",
- },
- {
- title: "webOS",
- ua: "Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pre/1.1",
- expected: "Mozilla:5.0 Platform:webOS OS:Palm Localization:en-US Browser:webOS-1.0 Engine:AppleWebKit-532.2 Bot:false Mobile:true",
- },
- {
- title: "Android",
- ua: "Mozilla/5.0 (Linux; U; Android 1.5; de-; HTC Magic Build/PLAT-RC33) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 1.5 Localization:de- Browser:Android-3.1.2 Engine:AppleWebKit-528.5+ Bot:false Mobile:true",
- },
- {
- title: "BlackBerry",
- ua: "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, Like Gecko) Version/6.0.0.141 Mobile Safari/534.1+",
- expected: "Mozilla:5.0 Platform:BlackBerry OS:BlackBerry 9800 Localization:en Browser:BlackBerry-6.0.0.141 Engine:AppleWebKit-534.1+ Bot:false Mobile:true",
- expectedOS: &OSInfo{"BlackBerry 9800", "BlackBerry", "9800"},
- },
- {
- title: "BB10",
- ua: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.3+ (KHTML, like Gecko) Version/10.0.9.388 Mobile Safari/537.3+",
- expected: "Mozilla:5.0 Platform:BlackBerry OS:BlackBerry Browser:BlackBerry-10.0.9.388 Engine:AppleWebKit-537.3+ Bot:false Mobile:true",
- },
- {
- title: "Ericsson",
- ua: "Mozilla/5.0 (SymbianOS/9.4; U; Series60/5.0 Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 Safari/525",
- expected: "Mozilla:5.0 Platform:Symbian OS:SymbianOS/9.4 Browser:Symbian-3.0 Engine:AppleWebKit-525 Bot:false Mobile:true",
- expectedOS: &OSInfo{"SymbianOS/9.4", "SymbianOS", "9.4"},
- },
- {
- title: "ChromeAndroid",
- ua: "Mozilla/5.0 (Linux; Android 4.2.1; Galaxy Nexus Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.1 Browser:Chrome-18.0.1025.166 Engine:AppleWebKit-535.19 Bot:false Mobile:true",
- },
- {
- title: "WebkitNoPlatform",
- ua: "Mozilla/5.0 (en-us) AppleWebKit/525.13 (KHTML, like Gecko; Google Web Preview) Version/3.1 Safari/525.13",
- expected: "Mozilla:5.0 Platform:en-us Localization:en-us Browser:Safari-3.1 Engine:AppleWebKit-525.13 Bot:false Mobile:false",
- },
- {
- title: "OperaWebkitMobile",
- ua: "Mozilla/5.0 (Linux; Android 4.2.2; Galaxy Nexus Build/JDQ39) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.58 Mobile Safari/537.31 OPR/14.0.1074.57453",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.2 Browser:Opera-14.0.1074.57453 Engine:AppleWebKit-537.31 Bot:false Mobile:true",
- },
- {
- title: "OperaWebkitDesktop",
- ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.58 Safari/537.31 OPR/14.0.1074.57453",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Opera-14.0.1074.57453 Engine:AppleWebKit-537.31 Bot:false Mobile:false",
- },
- {
- title: "ChromeNothingAfterU",
- ua: "Mozilla/5.0 (Linux; U) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4",
- expected: "Mozilla:5.0 Platform:Linux OS:Linux Browser:Chrome-22.0.1229.79 Engine:AppleWebKit-537.4 Bot:false Mobile:false",
- },
- {
- title: "SafariOnSymbian",
- ua: "Mozilla/5.0 (SymbianOS/9.1; U; [en-us]) AppleWebKit/413 (KHTML, like Gecko) Safari/413",
- expected: "Mozilla:5.0 Platform:Symbian OS:SymbianOS/9.1 Browser:Symbian-413 Engine:AppleWebKit-413 Bot:false Mobile:true",
- },
- {
- title: "Chromium - Ubuntu V49",
- ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chromium-49.0.2623.108 Engine:AppleWebKit-537.36 Bot:false Mobile:false",
- },
- {
- title: "Chromium - Ubuntu V55",
- ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/53.0.2785.143 Chrome/53.0.2785.143 Safari/537.36",
- expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chromium-53.0.2785.143 Engine:AppleWebKit-537.36 Bot:false Mobile:false",
- },
-
- // Dalvik
- {
- title: "Dalvik - Dell:001DL",
- ua: "Dalvik/1.2.0 (Linux; U; Android 2.2.2; 001DL Build/FRG83G)",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 2.2.2 Bot:false Mobile:true",
- },
- {
- title: "Dalvik - HTC:001HT",
- ua: "Dalvik/1.4.0 (Linux; U; Android 2.3.3; 001HT Build/GRI40)",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 2.3.3 Bot:false Mobile:true",
- },
- {
- title: "Dalvik - ZTE:009Z",
- ua: "Dalvik/1.4.0 (Linux; U; Android 2.3.4; 009Z Build/GINGERBREAD)",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 2.3.4 Bot:false Mobile:true",
- },
- {
- title: "Dalvik - A850",
- ua: "Dalvik/1.6.0 (Linux; U; Android 4.2.2; A850 Build/JDQ39) Configuration/CLDC-1.1; Opera Mini/att/4.2",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.2 Bot:false Mobile:true",
- },
- {
- title: "Dalvik - Asus:T00Q",
- ua: "Dalvik/1.6.0 (Linux; U; Android 4.4.2; ASUS_T00Q Build/KVT49L)/CLDC-1.1",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 4.4.2 Bot:false Mobile:true",
- expectedOS: &OSInfo{"Android 4.4.2", "Android", "4.4.2"},
- },
- {
- title: "Dalvik - W2430",
- ua: "Dalvik/1.6.0 (Linux; U; Android 4.0.4; W2430 Build/IMM76D)014; Profile/MIDP-2.1 Configuration/CLDC-1",
- expected: "Mozilla:5.0 Platform:Linux OS:Android 4.0.4 Bot:false Mobile:true",
- },
-}
-
-// Internal: beautify the UserAgent reference into a string so it can be
-// tested later on.
-//
-// ua - a UserAgent reference.
-//
-// Returns a string that contains the beautified representation.
-func beautify(ua *UserAgent) (s string) {
- if len(ua.Mozilla()) > 0 {
- s += "Mozilla:" + ua.Mozilla() + " "
- }
- if len(ua.Platform()) > 0 {
- s += "Platform:" + ua.Platform() + " "
- }
- if len(ua.OS()) > 0 {
- s += "OS:" + ua.OS() + " "
- }
- if len(ua.Localization()) > 0 {
- s += "Localization:" + ua.Localization() + " "
- }
- str1, str2 := ua.Browser()
- if len(str1) > 0 {
- s += "Browser:" + str1
- if len(str2) > 0 {
- s += "-" + str2 + " "
- } else {
- s += " "
- }
- }
- str1, str2 = ua.Engine()
- if len(str1) > 0 {
- s += "Engine:" + str1
- if len(str2) > 0 {
- s += "-" + str2 + " "
- } else {
- s += " "
- }
- }
- s += "Bot:" + fmt.Sprintf("%v", ua.Bot()) + " "
- s += "Mobile:" + fmt.Sprintf("%v", ua.Mobile())
- return s
-}
-
-// The test suite.
-func TestUserAgent(t *testing.T) {
- for _, tt := range uastrings {
- ua := New(tt.ua)
- got := beautify(ua)
- if tt.expected != got {
- t.Errorf("\nTest %v\ngot: %q\nexpected %q\n", tt.title, got, tt.expected)
- }
-
- if tt.expectedOS != nil {
- gotOSInfo := ua.OSInfo()
- if !reflect.DeepEqual(tt.expectedOS, &gotOSInfo) {
- t.Errorf("\nTest %v\ngot: %#v\nexpected %#v\n", tt.title, gotOSInfo, tt.expectedOS)
- }
- }
- }
-}
-
-// Benchmark: it parses each User-Agent string on the uastrings slice b.N times.
-func BenchmarkUserAgent(b *testing.B) {
- for i := 0; i < b.N; i++ {
- b.StopTimer()
- for _, tt := range uastrings {
- ua := new(UserAgent)
- b.StartTimer()
- ua.Parse(tt.ua)
- }
- }
-}
diff --git a/vendor/github.com/mssola/user_agent/bot.go b/vendor/github.com/mssola/user_agent/bot.go
deleted file mode 100644
index acccd51d6..000000000
--- a/vendor/github.com/mssola/user_agent/bot.go
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright (C) 2014-2018 Miquel Sabaté Solà <mikisabate@gmail.com>
-// This file is licensed under the MIT license.
-// See the LICENSE file.
-
-package user_agent
-
-import (
- "regexp"
- "strings"
-)
-
-var botFromSiteRegexp = regexp.MustCompile("http://.+\\.\\w+")
-
-// Get the name of the bot from the website that may be in the given comment. If
-// there is no website in the comment, then an empty string is returned.
-func getFromSite(comment []string) string {
- if len(comment) == 0 {
- return ""
- }
-
- // Where we should check the website.
- idx := 2
- if len(comment) < 3 {
- idx = 0
- }
-
- // Pick the site.
- results := botFromSiteRegexp.FindStringSubmatch(comment[idx])
- if len(results) == 1 {
- // If it's a simple comment, just return the name of the site.
- if idx == 0 {
- return results[0]
- }
-
- // This is a large comment, usually the name will be in the previous
- // field of the comment.
- return strings.TrimSpace(comment[1])
- }
- return ""
-}
-
-// Returns true if the info that we currently have corresponds to the Google
-// mobile bot. This function also modifies some attributes in the receiver
-// accordingly.
-func (p *UserAgent) googleBot() bool {
- // This is a hackish way to detect Google's mobile bot.
- if strings.Index(p.ua, "Googlebot") != -1 {
- p.platform = ""
- p.undecided = true
- }
- return p.undecided
-}
-
-// Set the attributes of the receiver as given by the parameters. All the other
-// parameters are set to empty.
-func (p *UserAgent) setSimple(name, version string, bot bool) {
- p.bot = bot
- if !bot {
- p.mozilla = ""
- }
- p.browser.Name = name
- p.browser.Version = version
- p.browser.Engine = ""
- p.browser.EngineVersion = ""
- p.os = ""
- p.localization = ""
-}
-
-// Fix some values for some weird browsers.
-func (p *UserAgent) fixOther(sections []section) {
- if len(sections) > 0 {
- p.browser.Name = sections[0].name
- p.browser.Version = sections[0].version
- p.mozilla = ""
- }
-}
-
-var botRegex = regexp.MustCompile("(?i)(bot|crawler|sp(i|y)der|search|worm|fetch|nutch)")
-
-// Check if we're dealing with a bot or with some weird browser. If that is the
-// case, the receiver will be modified accordingly.
-func (p *UserAgent) checkBot(sections []section) {
- // If there's only one element, and it's doesn't have the Mozilla string,
- // check whether this is a bot or not.
- if len(sections) == 1 && sections[0].name != "Mozilla" {
- p.mozilla = ""
-
- // Check whether the name has some suspicious "bot" or "crawler" in his name.
- if botRegex.Match([]byte(sections[0].name)) {
- p.setSimple(sections[0].name, "", true)
- return
- }
-
- // Tough luck, let's try to see if it has a website in his comment.
- if name := getFromSite(sections[0].comment); name != "" {
- // First of all, this is a bot. Moreover, since it doesn't have the
- // Mozilla string, we can assume that the name and the version are
- // the ones from the first section.
- p.setSimple(sections[0].name, sections[0].version, true)
- return
- }
-
- // At this point we are sure that this is not a bot, but some weirdo.
- p.setSimple(sections[0].name, sections[0].version, false)
- } else {
- // Let's iterate over the available comments and check for a website.
- for _, v := range sections {
- if name := getFromSite(v.comment); name != "" {
- // Ok, we've got a bot name.
- results := strings.SplitN(name, "/", 2)
- version := ""
- if len(results) == 2 {
- version = results[1]
- }
- p.setSimple(results[0], version, true)
- return
- }
- }
-
- // We will assume that this is some other weird browser.
- p.fixOther(sections)
- }
-}
diff --git a/vendor/github.com/mssola/user_agent/browser.go b/vendor/github.com/mssola/user_agent/browser.go
deleted file mode 100644
index c6a1d2469..000000000
--- a/vendor/github.com/mssola/user_agent/browser.go
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com>
-// This file is licensed under the MIT license.
-// See the LICENSE file.
-
-package user_agent
-
-import (
- "regexp"
- "strings"
-)
-
-var ie11Regexp = regexp.MustCompile("^rv:(.+)$")
-
-// A struct containing all the information that we might be
-// interested from the browser.
-type Browser struct {
- // The name of the browser's engine.
- Engine string
-
- // The version of the browser's engine.
- EngineVersion string
-
- // The name of the browser.
- Name string
-
- // The version of the browser.
- Version string
-}
-
-// Extract all the information that we can get from the User-Agent string
-// about the browser and update the receiver with this information.
-//
-// The function receives just one argument "sections", that contains the
-// sections from the User-Agent string after being parsed.
-func (p *UserAgent) detectBrowser(sections []section) {
- slen := len(sections)
-
- if sections[0].name == "Opera" {
- p.browser.Name = "Opera"
- p.browser.Version = sections[0].version
- p.browser.Engine = "Presto"
- if slen > 1 {
- p.browser.EngineVersion = sections[1].version
- }
- } else if sections[0].name == "Dalvik" {
- // When Dalvik VM is in use, there is no browser info attached to ua.
- // Although browser is still a Mozilla/5.0 compatible.
- p.mozilla = "5.0"
- } else if slen > 1 {
- engine := sections[1]
- p.browser.Engine = engine.name
- p.browser.EngineVersion = engine.version
- if slen > 2 {
- sectionIndex := 2
- // The version after the engine comment is empty on e.g. Ubuntu
- // platforms so if this is the case, let's use the next in line.
- if sections[2].version == "" && slen > 3 {
- sectionIndex = 3
- }
- p.browser.Version = sections[sectionIndex].version
- if engine.name == "AppleWebKit" {
- switch sections[slen-1].name {
- case "Edge":
- p.browser.Name = "Edge"
- p.browser.Version = sections[slen-1].version
- p.browser.Engine = "EdgeHTML"
- p.browser.EngineVersion = ""
- case "OPR":
- p.browser.Name = "Opera"
- p.browser.Version = sections[slen-1].version
- default:
- if sections[sectionIndex].name == "Chrome" {
- p.browser.Name = "Chrome"
- } else if sections[sectionIndex].name == "Chromium" {
- p.browser.Name = "Chromium"
- } else {
- p.browser.Name = "Safari"
- }
- }
- } else if engine.name == "Gecko" {
- name := sections[2].name
- if name == "MRA" && slen > 4 {
- name = sections[4].name
- p.browser.Version = sections[4].version
- }
- p.browser.Name = name
- } else if engine.name == "like" && sections[2].name == "Gecko" {
- // This is the new user agent from Internet Explorer 11.
- p.browser.Engine = "Trident"
- p.browser.Name = "Internet Explorer"
- for _, c := range sections[0].comment {
- version := ie11Regexp.FindStringSubmatch(c)
- if len(version) > 0 {
- p.browser.Version = version[1]
- return
- }
- }
- p.browser.Version = ""
- }
- }
- } else if slen == 1 && len(sections[0].comment) > 1 {
- comment := sections[0].comment
- if comment[0] == "compatible" && strings.HasPrefix(comment[1], "MSIE") {
- p.browser.Engine = "Trident"
- p.browser.Name = "Internet Explorer"
- // The MSIE version may be reported as the compatibility version.
- // For IE 8 through 10, the Trident token is more accurate.
- // http://msdn.microsoft.com/en-us/library/ie/ms537503(v=vs.85).aspx#VerToken
- for _, v := range comment {
- if strings.HasPrefix(v, "Trident/") {
- switch v[8:] {
- case "4.0":
- p.browser.Version = "8.0"
- case "5.0":
- p.browser.Version = "9.0"
- case "6.0":
- p.browser.Version = "10.0"
- }
- break
- }
- }
- // If the Trident token is not provided, fall back to MSIE token.
- if p.browser.Version == "" {
- p.browser.Version = strings.TrimSpace(comment[1][4:])
- }
- }
- }
-}
-
-// Returns two strings. The first string is the name of the engine and the
-// second one is the version of the engine.
-func (p *UserAgent) Engine() (string, string) {
- return p.browser.Engine, p.browser.EngineVersion
-}
-
-// Returns two strings. The first string is the name of the browser and the
-// second one is the version of the browser.
-func (p *UserAgent) Browser() (string, string) {
- return p.browser.Name, p.browser.Version
-}
diff --git a/vendor/github.com/mssola/user_agent/operating_systems.go b/vendor/github.com/mssola/user_agent/operating_systems.go
deleted file mode 100644
index ad4d56680..000000000
--- a/vendor/github.com/mssola/user_agent/operating_systems.go
+++ /dev/null
@@ -1,359 +0,0 @@
-// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com>
-// This file is licensed under the MIT license.
-// See the LICENSE file.
-
-package user_agent
-
-import "strings"
-
-// Represents full information on the operating system extracted from the user agent.
-type OSInfo struct {
- // Full name of the operating system. This is identical to the output of ua.OS()
- FullName string
-
- // Name of the operating system. This is sometimes a shorter version of the
- // operating system name, e.g. "Mac OS X" instead of "Intel Mac OS X"
- Name string
-
- // Operating system version, e.g. 7 for Windows 7 or 10.8 for Max OS X Mountain Lion
- Version string
-}
-
-// Normalize the name of the operating system. By now, this just
-// affects to Windows NT.
-//
-// Returns a string containing the normalized name for the Operating System.
-func normalizeOS(name string) string {
- sp := strings.SplitN(name, " ", 3)
- if len(sp) != 3 || sp[1] != "NT" {
- return name
- }
-
- switch sp[2] {
- case "5.0":
- return "Windows 2000"
- case "5.01":
- return "Windows 2000, Service Pack 1 (SP1)"
- case "5.1":
- return "Windows XP"
- case "5.2":
- return "Windows XP x64 Edition"
- case "6.0":
- return "Windows Vista"
- case "6.1":
- return "Windows 7"
- case "6.2":
- return "Windows 8"
- case "6.3":
- return "Windows 8.1"
- case "10.0":
- return "Windows 10"
- }
- return name
-}
-
-// Guess the OS, the localization and if this is a mobile device for a
-// Webkit-powered browser.
-//
-// The first argument p is a reference to the current UserAgent and the second
-// argument is a slice of strings containing the comment.
-func webkit(p *UserAgent, comment []string) {
- if p.platform == "webOS" {
- p.browser.Name = p.platform
- p.os = "Palm"
- if len(comment) > 2 {
- p.localization = comment[2]
- }
- p.mobile = true
- } else if p.platform == "Symbian" {
- p.mobile = true
- p.browser.Name = p.platform
- p.os = comment[0]
- } else if p.platform == "Linux" {
- p.mobile = true
- if p.browser.Name == "Safari" {
- p.browser.Name = "Android"
- }
- if len(comment) > 1 {
- if comment[1] == "U" {
- if len(comment) > 2 {
- p.os = comment[2]
- } else {
- p.mobile = false
- p.os = comment[0]
- }
- } else {
- p.os = comment[1]
- }
- }
- if len(comment) > 3 {
- p.localization = comment[3]
- } else if len(comment) == 3 {
- _ = p.googleBot()
- }
- } else if len(comment) > 0 {
- if len(comment) > 3 {
- p.localization = comment[3]
- }
- if strings.HasPrefix(comment[0], "Windows NT") {
- p.os = normalizeOS(comment[0])
- } else if len(comment) < 2 {
- p.localization = comment[0]
- } else if len(comment) < 3 {
- if !p.googleBot() {
- p.os = normalizeOS(comment[1])
- }
- } else {
- p.os = normalizeOS(comment[2])
- }
- if p.platform == "BlackBerry" {
- p.browser.Name = p.platform
- if p.os == "Touch" {
- p.os = p.platform
- }
- }
- }
-}
-
-// Guess the OS, the localization and if this is a mobile device
-// for a Gecko-powered browser.
-//
-// The first argument p is a reference to the current UserAgent and the second
-// argument is a slice of strings containing the comment.
-func gecko(p *UserAgent, comment []string) {
- if len(comment) > 1 {
- if comment[1] == "U" {
- if len(comment) > 2 {
- p.os = normalizeOS(comment[2])
- } else {
- p.os = normalizeOS(comment[1])
- }
- } else {
- if p.platform == "Android" {
- p.mobile = true
- p.platform, p.os = normalizeOS(comment[1]), p.platform
- } else if comment[0] == "Mobile" || comment[0] == "Tablet" {
- p.mobile = true
- p.os = "FirefoxOS"
- } else {
- if p.os == "" {
- p.os = normalizeOS(comment[1])
- }
- }
- }
- // Only parse 4th comment as localization if it doesn't start with rv:.
- // For example Firefox on Ubuntu contains "rv:XX.X" in this field.
- if len(comment) > 3 && !strings.HasPrefix(comment[3], "rv:") {
- p.localization = comment[3]
- }
- }
-}
-
-// Guess the OS, the localization and if this is a mobile device
-// for Internet Explorer.
-//
-// The first argument p is a reference to the current UserAgent and the second
-// argument is a slice of strings containing the comment.
-func trident(p *UserAgent, comment []string) {
- // Internet Explorer only runs on Windows.
- p.platform = "Windows"
-
- // The OS can be set before to handle a new case in IE11.
- if p.os == "" {
- if len(comment) > 2 {
- p.os = normalizeOS(comment[2])
- } else {
- p.os = "Windows NT 4.0"
- }
- }
-
- // Last but not least, let's detect if it comes from a mobile device.
- for _, v := range comment {
- if strings.HasPrefix(v, "IEMobile") {
- p.mobile = true
- return
- }
- }
-}
-
-// Guess the OS, the localization and if this is a mobile device
-// for Opera.
-//
-// The first argument p is a reference to the current UserAgent and the second
-// argument is a slice of strings containing the comment.
-func opera(p *UserAgent, comment []string) {
- slen := len(comment)
-
- if strings.HasPrefix(comment[0], "Windows") {
- p.platform = "Windows"
- p.os = normalizeOS(comment[0])
- if slen > 2 {
- if slen > 3 && strings.HasPrefix(comment[2], "MRA") {
- p.localization = comment[3]
- } else {
- p.localization = comment[2]
- }
- }
- } else {
- if strings.HasPrefix(comment[0], "Android") {
- p.mobile = true
- }
- p.platform = comment[0]
- if slen > 1 {
- p.os = comment[1]
- if slen > 3 {
- p.localization = comment[3]
- }
- } else {
- p.os = comment[0]
- }
- }
-}
-
-// Guess the OS. Android browsers send Dalvik as the user agent in the
-// request header.
-//
-// The first argument p is a reference to the current UserAgent and the second
-// argument is a slice of strings containing the comment.
-func dalvik(p *UserAgent, comment []string) {
- slen := len(comment)
-
- if strings.HasPrefix(comment[0], "Linux") {
- p.platform = comment[0]
- if slen > 2 {
- p.os = comment[2]
- }
- p.mobile = true
- }
-}
-
-// Given the comment of the first section of the UserAgent string,
-// get the platform.
-func getPlatform(comment []string) string {
- if len(comment) > 0 {
- if comment[0] != "compatible" {
- if strings.HasPrefix(comment[0], "Windows") {
- return "Windows"
- } else if strings.HasPrefix(comment[0], "Symbian") {
- return "Symbian"
- } else if strings.HasPrefix(comment[0], "webOS") {
- return "webOS"
- } else if comment[0] == "BB10" {
- return "BlackBerry"
- }
- return comment[0]
- }
- }
- return ""
-}
-
-// Detect some properties of the OS from the given section.
-func (p *UserAgent) detectOS(s section) {
- if s.name == "Mozilla" {
- // Get the platform here. Be aware that IE11 provides a new format
- // that is not backwards-compatible with previous versions of IE.
- p.platform = getPlatform(s.comment)
- if p.platform == "Windows" && len(s.comment) > 0 {
- p.os = normalizeOS(s.comment[0])
- }
-
- // And finally get the OS depending on the engine.
- switch p.browser.Engine {
- case "":
- p.undecided = true
- case "Gecko":
- gecko(p, s.comment)
- case "AppleWebKit":
- webkit(p, s.comment)
- case "Trident":
- trident(p, s.comment)
- }
- } else if s.name == "Opera" {
- if len(s.comment) > 0 {
- opera(p, s.comment)
- }
- } else if s.name == "Dalvik" {
- if len(s.comment) > 0 {
- dalvik(p, s.comment)
- }
- } else {
- // Check whether this is a bot or just a weird browser.
- p.undecided = true
- }
-}
-
-// Returns a string containing the platform..
-func (p *UserAgent) Platform() string {
- return p.platform
-}
-
-// Returns a string containing the name of the Operating System.
-func (p *UserAgent) OS() string {
- return p.os
-}
-
-// Returns a string containing the localization.
-func (p *UserAgent) Localization() string {
- return p.localization
-}
-
-// Return OS name and version from a slice of strings created from the full name of the OS.
-func osName(osSplit []string) (name, version string) {
- if len(osSplit) == 1 {
- name = osSplit[0]
- version = ""
- } else {
- // Assume version is stored in the last part of the array.
- nameSplit := osSplit[:len(osSplit)-1]
- version = osSplit[len(osSplit)-1]
-
- // Nicer looking Mac OS X
- if len(nameSplit) >= 2 && nameSplit[0] == "Intel" && nameSplit[1] == "Mac" {
- nameSplit = nameSplit[1:]
- }
- name = strings.Join(nameSplit, " ")
-
- if strings.Contains(version, "x86") || strings.Contains(version, "i686") {
- // x86_64 and i868 are not Linux versions but architectures
- version = ""
- } else if version == "X" && name == "Mac OS" {
- // X is not a version for Mac OS.
- name = name + " " + version
- version = ""
- }
- }
- return name, version
-}
-
-// Returns combined information for the operating system.
-func (p *UserAgent) OSInfo() OSInfo {
- // Special case for iPhone weirdness
- os := strings.Replace(p.os, "like Mac OS X", "", 1)
- os = strings.Replace(os, "CPU", "", 1)
- os = strings.Trim(os, " ")
-
- osSplit := strings.Split(os, " ")
-
- // Special case for x64 edition of Windows
- if os == "Windows XP x64 Edition" {
- osSplit = osSplit[:len(osSplit)-2]
- }
-
- name, version := osName(osSplit)
-
- // Special case for names that contain a forward slash version separator.
- if strings.Contains(name, "/") {
- s := strings.Split(name, "/")
- name = s[0]
- version = s[1]
- }
-
- // Special case for versions that use underscores
- version = strings.Replace(version, "_", ".", -1)
-
- return OSInfo{
- FullName: p.os,
- Name: name,
- Version: version,
- }
-}
diff --git a/vendor/github.com/mssola/user_agent/user_agent.go b/vendor/github.com/mssola/user_agent/user_agent.go
deleted file mode 100644
index 436e94980..000000000
--- a/vendor/github.com/mssola/user_agent/user_agent.go
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright (C) 2012-2018 Miquel Sabaté Solà <mikisabate@gmail.com>
-// This file is licensed under the MIT license.
-// See the LICENSE file.
-
-// Package user_agent implements an HTTP User Agent string parser. It defines
-// the type UserAgent that contains all the information from the parsed string.
-// It also implements the Parse function and getters for all the relevant
-// information that has been extracted from a parsed User Agent string.
-package user_agent
-
-import "strings"
-
-// A section contains the name of the product, its version and
-// an optional comment.
-type section struct {
- name string
- version string
- comment []string
-}
-
-// The UserAgent struct contains all the info that can be extracted
-// from the User-Agent string.
-type UserAgent struct {
- ua string
- mozilla string
- platform string
- os string
- localization string
- browser Browser
- bot bool
- mobile bool
- undecided bool
-}
-
-// Read from the given string until the given delimiter or the
-// end of the string have been reached.
-//
-// The first argument is the user agent string being parsed. The second
-// argument is a reference pointing to the current index of the user agent
-// string. The delimiter argument specifies which character is the delimiter
-// and the cat argument determines whether nested '(' should be ignored or not.
-//
-// Returns an array of bytes containing what has been read.
-func readUntil(ua string, index *int, delimiter byte, cat bool) []byte {
- var buffer []byte
-
- i := *index
- catalan := 0
- for ; i < len(ua); i = i + 1 {
- if ua[i] == delimiter {
- if catalan == 0 {
- *index = i + 1
- return buffer
- }
- catalan--
- } else if cat && ua[i] == '(' {
- catalan++
- }
- buffer = append(buffer, ua[i])
- }
- *index = i + 1
- return buffer
-}
-
-// Parse the given product, that is, just a name or a string
-// formatted as Name/Version.
-//
-// It returns two strings. The first string is the name of the product and the
-// second string contains the version of the product.
-func parseProduct(product []byte) (string, string) {
- prod := strings.SplitN(string(product), "/", 2)
- if len(prod) == 2 {
- return prod[0], prod[1]
- }
- return string(product), ""
-}
-
-// Parse a section. A section is typically formatted as follows
-// "Name/Version (comment)". Both, the comment and the version are optional.
-//
-// The first argument is the user agent string being parsed. The second
-// argument is a reference pointing to the current index of the user agent
-// string.
-//
-// Returns a section containing the information that we could extract
-// from the last parsed section.
-func parseSection(ua string, index *int) (s section) {
- buffer := readUntil(ua, index, ' ', false)
-
- s.name, s.version = parseProduct(buffer)
- if *index < len(ua) && ua[*index] == '(' {
- *index++
- buffer = readUntil(ua, index, ')', true)
- s.comment = strings.Split(string(buffer), "; ")
- *index++
- }
- return s
-}
-
-// Initialize the parser.
-func (p *UserAgent) initialize() {
- p.ua = ""
- p.mozilla = ""
- p.platform = ""
- p.os = ""
- p.localization = ""
- p.browser.Engine = ""
- p.browser.EngineVersion = ""
- p.browser.Name = ""
- p.browser.Version = ""
- p.bot = false
- p.mobile = false
- p.undecided = false
-}
-
-// Parse the given User-Agent string and get the resulting UserAgent object.
-//
-// Returns an UserAgent object that has been initialized after parsing
-// the given User-Agent string.
-func New(ua string) *UserAgent {
- o := &UserAgent{}
- o.Parse(ua)
- return o
-}
-
-// Parse the given User-Agent string. After calling this function, the
-// receiver will be setted up with all the information that we've extracted.
-func (p *UserAgent) Parse(ua string) {
- var sections []section
-
- p.initialize()
- p.ua = ua
- for index, limit := 0, len(ua); index < limit; {
- s := parseSection(ua, &index)
- if !p.mobile && s.name == "Mobile" {
- p.mobile = true
- }
- sections = append(sections, s)
- }
-
- if len(sections) > 0 {
- if sections[0].name == "Mozilla" {
- p.mozilla = sections[0].version
- }
-
- p.detectBrowser(sections)
- p.detectOS(sections[0])
-
- if p.undecided {
- p.checkBot(sections)
- }
- }
-}
-
-// Returns the mozilla version (it's how the User Agent string begins:
-// "Mozilla/5.0 ...", unless we're dealing with Opera, of course).
-func (p *UserAgent) Mozilla() string {
- return p.mozilla
-}
-
-// Returns true if it's a bot, false otherwise.
-func (p *UserAgent) Bot() bool {
- return p.bot
-}
-
-// Returns true if it's a mobile device, false otherwise.
-func (p *UserAgent) Mobile() bool {
- return p.mobile
-}
-
-// Returns the original given user agent.
-func (p *UserAgent) UA() string {
- return p.ua
-}
diff --git a/web/web.go b/web/web.go
index e0edd1b7a..22fe43923 100644
--- a/web/web.go
+++ b/web/web.go
@@ -8,12 +8,12 @@ import (
"strings"
"github.com/NYTimes/gziphandler"
+ "github.com/avct/uasurfer"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/mattermost-server/api"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
- "github.com/mssola/user_agent"
)
func Init(api3 *api.API) {
@@ -65,26 +65,27 @@ func pluginHandler(config model.ConfigFunc, handler http.Handler) http.Handler {
})
}
-var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8"
+// Due to the complexities of UA detection and the ramifications of a misdetection only older Safari and IE browsers throw incompatibility errors.
-func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
- ua := user_agent.New(r.UserAgent())
- bname, bversion := ua.Browser()
+// Map should be of minimum required browser version.
+var browserMinimumSupported = map[string]int{
+ "BrowserIE": 11,
+ "BrowserSafari": 9,
+}
- browsers := strings.Split(browsersNotSupported, ";")
- for _, browser := range browsers {
- version := strings.Split(browser, "/")
+func CheckClientCompatability(agentString string) bool {
+ ua := uasurfer.Parse(agentString)
- if strings.HasPrefix(bname, version[0]) && strings.HasPrefix(bversion, version[1]) {
- return false
- }
+ if version, exist := browserMinimumSupported[ua.Browser.Name.String()]; exist && ua.Browser.Version.Major < version {
+ return false
}
return true
}
func root(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !CheckBrowserCompatability(c, r) {
+
+ if !CheckClientCompatability(r.UserAgent()) {
w.Header().Set("Cache-Control", "no-store")
page := utils.NewHTMLTemplate(c.App.HTMLTemplates(), "unsupported_browser")
page.Props["Title"] = c.T("web.error.unsupported_browser.title")
diff --git a/web/web_test.go b/web/web_test.go
index 20c42245a..60122f049 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -168,4 +168,35 @@ func TestMain(m *testing.M) {
}()
status = m.Run()
+
+}
+
+func TestCheckClientCompatability(t *testing.T) {
+ //Browser Name, UA String, expected result (if the browser should fail the test false and if it should pass the true)
+ type uaTest struct {
+ Name string // Name of Browser
+ UserAgent string // Useragent of Browser
+ Result bool // Expected result (true if browser should be compatible, false if browser shouldn't be compatible)
+ }
+ var uaTestParameters = []uaTest{
+ {"Mozilla 40.1", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", true},
+ {"Chrome 60", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", true},
+ {"Chrome Mobile", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Mobile Safari/537.36", true},
+ {"MM Classic App", "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR6.170623.013; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/61.0.3163.81 Mobile Safari/537.36 Web-Atoms-Mobile-WebView", true},
+ {"MM App 3.7.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Mattermost/3.7.1 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36", true},
+ {"Franz 4.0.4", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/4.0.4 Chrome/52.0.2743.82 Electron/1.3.1 Safari/537.36", true},
+ {"Edge 14", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393", true},
+ {"Internet Explorer 11", "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", true},
+ {"Internet Explorer 9", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0", false},
+ {"Safari 9", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38", true},
+ {"Safari 8", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12", false},
+ {"Safari Mobile", "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B137 Safari/601.1", true},
+ }
+ for _, browser := range uaTestParameters {
+ t.Run(browser.Name, func(t *testing.T) {
+ if result := CheckClientCompatability(browser.UserAgent); result != browser.Result {
+ t.Fatalf("%s User Agent Test failed!", browser.Name)
+ }
+ })
+ }
}
diff --git a/wsapi/user.go b/wsapi/user.go
index 68fe27e0f..509ca8a14 100644
--- a/wsapi/user.go
+++ b/wsapi/user.go
@@ -29,7 +29,7 @@ func (api *API) userTyping(req *model.WebSocketRequest) (map[string]interface{},
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers)
event.Add("parent_id", parentId)
event.Add("user_id", req.Session.UserId)
- go api.App.Publish(event)
+ api.App.Publish(event)
return nil, nil
}
diff --git a/wsapi/webrtc.go b/wsapi/webrtc.go
index fbefb1b38..de50fa06b 100644
--- a/wsapi/webrtc.go
+++ b/wsapi/webrtc.go
@@ -20,7 +20,7 @@ func (api *API) webrtcMessage(req *model.WebSocketRequest) (map[string]interface
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_WEBRTC, "", "", toUserId, nil)
event.Data = req.Data
- go api.App.Publish(event)
+ api.App.Publish(event)
return nil, nil
}