diff options
-rw-r--r-- | api4/command.go | 26 | ||||
-rw-r--r-- | api4/command_test.go | 96 | ||||
-rw-r--r-- | app/import.go | 77 | ||||
-rw-r--r-- | app/import_test.go | 80 | ||||
-rw-r--r-- | app/post.go | 5 | ||||
-rw-r--r-- | i18n/en.json | 16 | ||||
-rw-r--r-- | model/emoji.go | 8 | ||||
-rw-r--r-- | model/emoji_test.go | 5 | ||||
-rw-r--r-- | model/user.go | 1 | ||||
-rw-r--r-- | model/user_test.go | 1 | ||||
-rw-r--r-- | store/sqlstore/upgrade.go | 10 |
11 files changed, 276 insertions, 49 deletions
diff --git a/api4/command.go b/api4/command.go index 3ab2839ba..69efee010 100644 --- a/api4/command.go +++ b/api4/command.go @@ -4,7 +4,6 @@ package api4 import ( - "io/ioutil" "net/http" "strconv" "strings" @@ -22,9 +21,6 @@ func (api *API) InitCommand() { api.BaseRoutes.Team.Handle("/commands/autocomplete", api.ApiSessionRequired(listAutocompleteCommands)).Methods("GET") api.BaseRoutes.Command.Handle("/regen_token", api.ApiSessionRequired(regenCommandToken)).Methods("PUT") - - api.BaseRoutes.Teams.Handle("/command_test", api.ApiHandler(testCommand)).Methods("POST") - api.BaseRoutes.Teams.Handle("/command_test", api.ApiHandler(testCommand)).Methods("GET") } func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { @@ -291,25 +287,3 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(resp))) } - -func testCommand(c *Context, w http.ResponseWriter, r *http.Request) { - r.ParseForm() - - msg := "" - if r.Method == "POST" { - msg = msg + "\ntoken=" + r.FormValue("token") - msg = msg + "\nteam_domain=" + r.FormValue("team_domain") - } else { - body, _ := ioutil.ReadAll(r.Body) - msg = string(body) - } - - rc := &model.CommandResponse{ - Text: "test command response " + msg, - ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, - Type: "custom_test", - Props: map[string]interface{}{"someprop": "somevalue"}, - } - - w.Write([]byte(rc.ToJson())) -} diff --git a/api4/command_test.go b/api4/command_test.go index 0d37d7440..96025c063 100644 --- a/api4/command_test.go +++ b/api4/command_test.go @@ -4,7 +4,6 @@ package api4 import ( - "fmt" "net/http" "net/http/httptest" "net/url" @@ -423,7 +422,7 @@ func TestExecuteInvalidCommand(t *testing.T) { getCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: th.BasicTeam.Id, - URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4), + URL: ts.URL, Method: model.COMMAND_METHOD_GET, Trigger: "getcommand", } @@ -501,7 +500,7 @@ func TestExecuteGetCommand(t *testing.T) { getCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: th.BasicTeam.Id, - URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4), + URL: ts.URL, Method: model.COMMAND_METHOD_GET, Trigger: "getcommand", Token: token, @@ -556,16 +555,16 @@ func TestExecutePostCommand(t *testing.T) { })) defer ts.Close() - getCmd := &model.Command{ + postCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: th.BasicTeam.Id, - URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4), + URL: ts.URL, Method: model.COMMAND_METHOD_POST, Trigger: "postcommand", Token: token, } - if _, err := th.App.CreateCommand(getCmd); err != nil { + if _, err := th.App.CreateCommand(postCmd); err != nil { t.Fatal("failed to create get command") } @@ -592,14 +591,29 @@ func TestExecuteCommandAgainstChannelOnAnotherTeam(t *testing.T) { }) }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" + }) + + expectedCommandResponse := &model.CommandResponse{ + Text: "test post command response", + ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, + Type: "custom_test", + Props: map[string]interface{}{"someprop": "somevalue"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(expectedCommandResponse.ToJson())) + })) + defer ts.Close() // create a slash command on some other team where we have permission to do so team2 := th.CreateTeam() postCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: team2.Id, - URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test", + URL: ts.URL, Method: model.COMMAND_METHOD_POST, Trigger: "postcommand", } @@ -627,14 +641,29 @@ func TestExecuteCommandAgainstChannelUserIsNotIn(t *testing.T) { }) }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" + }) + + expectedCommandResponse := &model.CommandResponse{ + Text: "test post command response", + ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, + Type: "custom_test", + Props: map[string]interface{}{"someprop": "somevalue"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(expectedCommandResponse.ToJson())) + })) + defer ts.Close() // create a slash command on some other team where we have permission to do so team2 := th.CreateTeam() postCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: team2.Id, - URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test", + URL: ts.URL, Method: model.COMMAND_METHOD_POST, Trigger: "postcommand", } @@ -667,14 +696,32 @@ func TestExecuteCommandInDirectMessageChannel(t *testing.T) { }) }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" + }) - // create a slash command on some other team where we have permission to do so + // create a team that the user isn't a part of team2 := th.CreateTeam() + + expectedCommandResponse := &model.CommandResponse{ + Text: "test post command response", + ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, + Type: "custom_test", + Props: map[string]interface{}{"someprop": "somevalue"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(expectedCommandResponse.ToJson())) + })) + defer ts.Close() + + // create a slash command on some other team where we have permission to do so postCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: team2.Id, - URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test", + URL: ts.URL, Method: model.COMMAND_METHOD_POST, Trigger: "postcommand", } @@ -709,16 +756,35 @@ func TestExecuteCommandInTeamUserIsNotOn(t *testing.T) { }) }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" + }) // create a team that the user isn't a part of team2 := th.CreateTeam() + expectedCommandResponse := &model.CommandResponse{ + Text: "test post command response", + ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, + Type: "custom_test", + Props: map[string]interface{}{"someprop": "somevalue"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + r.ParseForm() + require.Equal(t, team2.Name, r.FormValue("team_domain")) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(expectedCommandResponse.ToJson())) + })) + defer ts.Close() + // create a slash command on that team postCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: team2.Id, - URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test", + URL: ts.URL, Method: model.COMMAND_METHOD_POST, Trigger: "postcommand", } diff --git a/app/import.go b/app/import.go index baf936567..12353d562 100644 --- a/app/import.go +++ b/app/import.go @@ -33,6 +33,7 @@ type LineImportData struct { Post *PostImportData `json:"post"` DirectChannel *DirectChannelImportData `json:"direct_channel"` DirectPost *DirectPostImportData `json:"direct_post"` + Emoji *EmojiImportData `json:"emoji"` Version *int `json:"version"` } @@ -114,6 +115,11 @@ type UserChannelNotifyPropsImportData struct { MarkUnread *string `json:"mark_unread"` } +type EmojiImportData struct { + Name *string `json:"name"` + Image *string `json:"image"` +} + type ReactionImportData struct { User *string `json:"user"` CreateAt *int64 `json:"create_at"` @@ -337,6 +343,12 @@ func (a *App) ImportLine(line LineImportData, dryRun bool) *model.AppError { } else { return a.ImportDirectPost(line.DirectPost, dryRun) } + case line.Type == "emoji": + if line.Emoji == nil { + return model.NewAppError("BulkImport", "app.import.import_line.null_emoji.error", nil, "", http.StatusBadRequest) + } else { + return a.ImportEmoji(line.Emoji, dryRun) + } default: return model.NewAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]interface{}{"Type": line.Type}, "", http.StatusBadRequest) } @@ -1925,6 +1937,71 @@ func validateDirectPostImportData(data *DirectPostImportData, maxPostSize int) * return nil } +func (a *App) ImportEmoji(data *EmojiImportData, dryRun bool) *model.AppError { + if err := validateEmojiImportData(data); err != nil { + return err + } + + // If this is a Dry Run, do not continue any further. + if dryRun { + return nil + } + + var emoji *model.Emoji + var err *model.AppError + + emoji, err = a.GetEmojiByName(*data.Name) + if err != nil && err.StatusCode != http.StatusNotFound { + return err + } + + alreadyExists := emoji != nil + + if !alreadyExists { + emoji = &model.Emoji{ + Name: *data.Name, + } + emoji.PreSave() + } + + file, fileErr := os.Open(*data.Image) + if fileErr != nil { + return model.NewAppError("BulkImport", "app.import.emoji.bad_file.error", map[string]interface{}{"EmojiName": *data.Name}, "", http.StatusBadRequest) + } + + if _, err := a.WriteFile(file, getEmojiImagePath(emoji.Id)); err != nil { + return err + } + + if !alreadyExists { + if result := <-a.Srv.Store.Emoji().Save(emoji); result.Err != nil { + return result.Err + } + } + + return nil +} + +func validateEmojiImportData(data *EmojiImportData) *model.AppError { + if data == nil { + return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.empty.error", nil, "", http.StatusBadRequest) + } + + if data.Name == nil || len(*data.Name) == 0 { + return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.name_missing.error", nil, "", http.StatusBadRequest) + } + + if err := model.IsValidEmojiName(*data.Name); err != nil { + return err + } + + if data.Image == nil || len(*data.Image) == 0 { + return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.image_missing.error", nil, "", http.StatusBadRequest) + } + + return nil +} + // // -- Old SlackImport Functions -- // Import functions are sutible for entering posts and users into the database without diff --git a/app/import_test.go b/app/import_test.go index e7bc055a4..8a88937f9 100644 --- a/app/import_test.go +++ b/app/import_test.go @@ -3774,11 +3774,16 @@ func TestImportBulkImport(t *testing.T) { th := Setup() defer th.TearDown() + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true }) + teamName := model.NewId() channelName := model.NewId() username := model.NewId() username2 := model.NewId() username3 := model.NewId() + emojiName := model.NewId() + testsDir, _ := utils.FindDir("tests") + testImage := filepath.Join(testsDir, "test.png") // Run bulk import with a valid 1 of everything. data1 := `{"type": "version", "version": 1} @@ -3791,7 +3796,8 @@ func TestImportBulkImport(t *testing.T) { {"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username2 + `"]}} {"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username2 + `", "` + username3 + `"]}} {"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `"], "user": "` + username + `", "message": "Hello Direct Channel", "create_at": 123456789013}} -{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789014}}` +{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789014}} +{"type": "emoji", "emoji": {"name": "` + emojiName + `", "image": "` + testImage + `"}}` if err, line := th.App.BulkImport(strings.NewReader(data1), false, 2); err != nil || line != 0 { t.Fatalf("BulkImport should have succeeded: %v, %v", err.Error(), line) @@ -3833,3 +3839,75 @@ func TestImportProcessImportDataFileVersionLine(t *testing.T) { t.Fatalf("Expected error on invalid version line.") } } + +func TestImportValidateEmojiImportData(t *testing.T) { + data := EmojiImportData{ + Name: ptrStr("parrot"), + Image: ptrStr("/path/to/image"), + } + + err := validateEmojiImportData(&data) + assert.Nil(t, err, "Validation should succeed") + + *data.Name = "smiley" + err = validateEmojiImportData(&data) + assert.NotNil(t, err) + + *data.Name = "" + err = validateEmojiImportData(&data) + assert.NotNil(t, err) + + *data.Name = "" + *data.Image = "" + err = validateEmojiImportData(&data) + assert.NotNil(t, err) + + *data.Image = "/path/to/image" + data.Name = nil + err = validateEmojiImportData(&data) + assert.NotNil(t, err) + + data.Name = ptrStr("parrot") + data.Image = nil + err = validateEmojiImportData(&data) + assert.NotNil(t, err) +} + +func TestImportImportEmoji(t *testing.T) { + th := Setup() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true }) + + testsDir, _ := utils.FindDir("tests") + testImage := filepath.Join(testsDir, "test.png") + + data := EmojiImportData{Name: ptrStr(model.NewId())} + err := th.App.ImportEmoji(&data, true) + assert.NotNil(t, err, "Invalid emoji should have failed dry run") + + result := <-th.App.Srv.Store.Emoji().GetByName(*data.Name) + assert.Nil(t, result.Data, "Emoji should not have been imported") + + data.Image = ptrStr(testImage) + err = th.App.ImportEmoji(&data, true) + assert.Nil(t, err, "Valid emoji should have passed dry run") + + data = EmojiImportData{Name: ptrStr(model.NewId())} + err = th.App.ImportEmoji(&data, false) + assert.NotNil(t, err, "Invalid emoji should have failed apply mode") + + data.Image = ptrStr("non-existent-file") + err = th.App.ImportEmoji(&data, false) + assert.NotNil(t, err, "Emoji with bad image file should have failed apply mode") + + data.Image = ptrStr(testImage) + err = th.App.ImportEmoji(&data, false) + assert.Nil(t, err, "Valid emoji should have succeeded apply mode") + + result = <-th.App.Srv.Store.Emoji().GetByName(*data.Name) + assert.NotNil(t, result.Data, "Emoji should have been imported") + + err = th.App.ImportEmoji(&data, false) + assert.Nil(t, err, "Second run should have succeeded apply mode") +} diff --git a/app/post.go b/app/post.go index e24018995..806263f5f 100644 --- a/app/post.go +++ b/app/post.go @@ -765,6 +765,11 @@ func (a *App) GetOpenGraphMetadata(requestURL string) *opengraph.OpenGraph { makeOpenGraphURLsAbsolute(og, requestURL) + // The URL should be the link the user provided in their message, not a redirected one. + if og.URL != "" { + og.URL = requestURL + } + return og } diff --git a/i18n/en.json b/i18n/en.json index 80693edbf..fc6d1c55b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2391,6 +2391,22 @@ "translation": "Cluster API endpoint not found." }, { + "id": "app.import.emoji.bad_file.error", + "translation": "Error reading import emoji image file. Emoji with name: \"{{.EmojiName}}\"" + }, + { + "id": "app.import.validate_emoji_import_data.empty.error", + "translation": "Import emoji data empty." + }, + { + "id": "app.import.validate_emoji_import_data.name_missing.error", + "translation": "Import emoji name field missing or blank." + }, + { + "id": "app.import.validate_emoji_import_data.image_missing.error", + "translation": "Import emoji image field missing or blank." + }, + { "id": "app.import.bulk_import.file_scan.error", "translation": "Error reading import data file." }, diff --git a/model/emoji.go b/model/emoji.go index 78a266386..f14af89df 100644 --- a/model/emoji.go +++ b/model/emoji.go @@ -41,11 +41,15 @@ func (emoji *Emoji) IsValid() *AppError { return NewAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id, http.StatusBadRequest) } - if len(emoji.CreatorId) != 26 { + if len(emoji.CreatorId) > 26 { return NewAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "", http.StatusBadRequest) } - if len(emoji.Name) == 0 || len(emoji.Name) > EMOJI_NAME_MAX_LENGTH || !IsValidAlphaNumHyphenUnderscore(emoji.Name, false) || inSystemEmoji(emoji.Name) { + return IsValidEmojiName(emoji.Name) +} + +func IsValidEmojiName(name string) *AppError { + if len(name) == 0 || len(name) > EMOJI_NAME_MAX_LENGTH || !IsValidAlphaNumHyphenUnderscore(name, false) || inSystemEmoji(name) { return NewAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "", http.StatusBadRequest) } diff --git a/model/emoji_test.go b/model/emoji_test.go index 95abe37c6..50d741214 100644 --- a/model/emoji_test.go +++ b/model/emoji_test.go @@ -40,11 +40,6 @@ func TestEmojiIsValid(t *testing.T) { } emoji.UpdateAt = 1234 - emoji.CreatorId = strings.Repeat("1", 25) - if err := emoji.IsValid(); err == nil { - t.Fatal() - } - emoji.CreatorId = strings.Repeat("1", 27) if err := emoji.IsValid(); err == nil { t.Fatal() diff --git a/model/user.go b/model/user.go index c5d6c13b6..e56f3aaed 100644 --- a/model/user.go +++ b/model/user.go @@ -565,6 +565,7 @@ var restrictedUsernames = []string{ "all", "channel", "matterbot", + "system", } func IsValidUsername(s string) bool { diff --git a/model/user_test.go b/model/user_test.go index 645eaadff..a1953a40d 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -272,6 +272,7 @@ var usernames = []struct { {"spin'punch", false}, {"spin*punch", false}, {"all", false}, + {"system", false}, } func TestValidUsername(t *testing.T) { diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 868575522..8ea44371c 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -15,6 +15,7 @@ import ( ) const ( + VERSION_5_2_0 = "5.2.0" VERSION_5_1_0 = "5.1.0" VERSION_5_0_0 = "5.0.0" VERSION_4_10_0 = "4.10.0" @@ -80,6 +81,7 @@ func UpgradeDatabase(sqlStore SqlStore) { UpgradeDatabaseToVersion410(sqlStore) UpgradeDatabaseToVersion50(sqlStore) UpgradeDatabaseToVersion51(sqlStore) + UpgradeDatabaseToVersion52(sqlStore) // If the SchemaVersion is empty this this is the first time it has ran // so lets set it to the current version. @@ -470,3 +472,11 @@ func UpgradeDatabaseToVersion51(sqlStore SqlStore) { saveSchemaVersion(sqlStore, VERSION_5_1_0) } } + +func UpgradeDatabaseToVersion52(sqlStore SqlStore) { + // TODO: Uncomment following condition when version 5.2.0 is released + // if shouldPerformUpgrade(sqlStore, VERSION_5_1_0, VERSION_5_2_0) { + + // saveSchemaVersion(sqlStore, VERSION_5_2_0) + // } +} |