diff --git a/app/import_functions.go b/app/import_functions.go index 81acf0380c..6afa58acd7 100644 --- a/app/import_functions.go +++ b/app/import_functions.go @@ -492,7 +492,7 @@ func (a *App) ImportUser(data *UserImportData, dryRun bool) *model.AppError { if err != nil { mlog.Error("Unable to open the profile image.", mlog.Any("err", err)) } - if err := a.SetProfileImageFromFile(savedUser.Id, file); err != nil { + if err := a.SetProfileImageFromMultiPartFile(savedUser.Id, file); err != nil { mlog.Error("Unable to set the profile image from a file.", mlog.Any("err", err)) } } diff --git a/app/plugin_api.go b/app/plugin_api.go index 6aae2e618c..6d6de42f84 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -4,6 +4,7 @@ package app import ( + "bytes" "encoding/json" "fmt" "net/http" @@ -389,6 +390,20 @@ func (api *PluginAPI) GetProfileImage(userId string) ([]byte, *model.AppError) { return data, err } +func (api *PluginAPI) SetProfileImage(userId string, data []byte) *model.AppError { + _, err := api.app.GetUser(userId) + if err != nil { + return err + } + + fileReader := bytes.NewReader(data) + err = api.app.SetProfileImageFromFile(userId, fileReader) + if err != nil { + return err + } + return nil +} + func (api *PluginAPI) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) { return api.app.GetEmojiList(page, perPage, sortBy) } @@ -438,6 +453,19 @@ func (api *PluginAPI) GetEmojiImage(emojiId string) ([]byte, string, *model.AppE return api.app.GetEmojiImage(emojiId) } +func (api *PluginAPI) GetTeamIcon(teamId string) ([]byte, *model.AppError) { + team, err := api.app.GetTeam(teamId) + if err != nil { + return nil, err + } + + data, err := api.app.GetTeamIcon(team) + if err != nil { + return nil, err + } + return data, nil +} + // Plugin Section func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) { diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 7311348990..e195718bf8 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -4,8 +4,12 @@ package app import ( + "bytes" "encoding/json" "fmt" + "image" + "image/color" + "image/png" "io/ioutil" "os" "path/filepath" @@ -220,6 +224,36 @@ func TestPluginAPIGetProfileImage(t *testing.T) { require.Nil(t, data) } +func TestPluginAPISetProfileImage(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + // Create an 128 x 128 image + img := image.NewRGBA(image.Rect(0, 0, 128, 128)) + // Draw a red dot at (2, 3) + img.Set(2, 3, color.RGBA{255, 0, 0, 255}) + buf := new(bytes.Buffer) + err := png.Encode(buf, img) + require.Nil(t, err) + dataBytes := buf.Bytes() + + // Set the user profile image + err = api.SetProfileImage(th.BasicUser.Id, dataBytes) + require.Nil(t, err) + + // Get the user profile image to check + imageProfile, err := api.GetProfileImage(th.BasicUser.Id) + require.Nil(t, err) + require.NotEmpty(t, imageProfile) + + colorful := color.NRGBA{255, 0, 0, 255} + byteReader := bytes.NewReader(imageProfile) + img2, _, err2 := image.Decode(byteReader) + require.Nil(t, err2) + require.Equal(t, img2.At(2, 3), colorful) +} + func TestPluginAPIGetPlugins(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -277,3 +311,34 @@ func TestPluginAPIGetPlugins(t *testing.T) { assert.NotEmpty(t, plugins) assert.Equal(t, pluginManifests, plugins) } + +func TestPluginAPIGetTeamIcon(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + // Create an 128 x 128 image + img := image.NewRGBA(image.Rect(0, 0, 128, 128)) + // Draw a red dot at (2, 3) + img.Set(2, 3, color.RGBA{255, 0, 0, 255}) + buf := new(bytes.Buffer) + err := png.Encode(buf, img) + require.Nil(t, err) + dataBytes := buf.Bytes() + fileReader := bytes.NewReader(dataBytes) + + // Set the Team Icon + err = th.App.SetTeamIconFromFile(th.BasicTeam, fileReader) + require.Nil(t, err) + + // Get the team icon to check + imageProfile, err := api.GetTeamIcon(th.BasicTeam.Id) + require.Nil(t, err) + require.NotEmpty(t, imageProfile) + + colorful := color.NRGBA{255, 0, 0, 255} + byteReader := bytes.NewReader(imageProfile) + img2, _, err2 := image.Decode(byteReader) + require.Nil(t, err2) + require.Equal(t, img2.At(2, 3), colorful) +} diff --git a/app/post.go b/app/post.go index dd07a5b6a8..75661ca349 100644 --- a/app/post.go +++ b/app/post.go @@ -605,7 +605,7 @@ func (a *App) DeleteFlaggedPosts(postId string) { } func (a *App) DeletePostFiles(post *model.Post) { - if len(post.FileIds) != 0 { + if len(post.FileIds) == 0 { return } diff --git a/app/post_test.go b/app/post_test.go index a941a549ab..5ca3977174 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -537,3 +537,48 @@ func TestMaxPostSize(t *testing.T) { }) } } + +func TestDeletePostWithFileAttachments(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + // Create a post with a file attachment. + teamId := th.BasicTeam.Id + channelId := th.BasicChannel.Id + userId := th.BasicUser.Id + filename := "test" + data := []byte("abcd") + + info1, err := th.App.DoUploadFile(time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamId, channelId, userId, filename, data) + if err != nil { + t.Fatal(err) + } else { + defer func() { + <-th.App.Srv.Store.FileInfo().PermanentDelete(info1.Id) + th.App.RemoveFile(info1.Path) + }() + } + + post := &model.Post{ + Message: "asd", + ChannelId: channelId, + PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), + UserId: userId, + CreateAt: 0, + FileIds: []string{info1.Id}, + } + + post, err = th.App.CreatePost(post, th.BasicChannel, false) + assert.Nil(t, err) + + // Delete the post. + post, err = th.App.DeletePost(post.Id, userId) + assert.Nil(t, err) + + // Wait for the cleanup routine to finish. + time.Sleep(time.Millisecond * 100) + + // Check that the file can no longer be reached. + _, err = th.App.GetFileInfo(info1.Id) + assert.NotNil(t, err) +} diff --git a/app/team.go b/app/team.go index 6180611048..0152cbb6af 100644 --- a/app/team.go +++ b/app/team.go @@ -8,6 +8,7 @@ import ( "fmt" "image" "image/png" + "io" "mime/multipart" "net/http" "net/url" @@ -1093,10 +1094,10 @@ func (a *App) SetTeamIcon(teamId string, imageData *multipart.FileHeader) *model return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, err.Error(), http.StatusBadRequest) } defer file.Close() - return a.SetTeamIconFromFile(teamId, file) + return a.SetTeamIconFromMultiPartFile(teamId, file) } -func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.AppError { +func (a *App) SetTeamIconFromMultiPartFile(teamId string, file multipart.File) *model.AppError { team, getTeamErr := a.GetTeam(teamId) if getTeamErr != nil { @@ -1118,14 +1119,16 @@ func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.App file.Seek(0, 0) + return a.SetTeamIconFromFile(team, file) +} + +func (a *App) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError { // Decode image into Image object img, _, err := image.Decode(file) if err != nil { return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, err.Error(), http.StatusBadRequest) } - file.Seek(0, 0) - orientation, _ := getImageOrientation(file) img = makeImageUpright(img, orientation) @@ -1139,7 +1142,7 @@ func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.App return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, err.Error(), http.StatusInternalServerError) } - path := "teams/" + teamId + "/teamIcon.png" + path := "teams/" + team.Id + "/teamIcon.png" if _, err := a.WriteFile(buf, path); err != nil { return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError) @@ -1147,7 +1150,7 @@ func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.App curTime := model.GetMillis() - if result := <-a.Srv.Store.Team().UpdateLastTeamIconUpdate(teamId, curTime); result.Err != nil { + if result := <-a.Srv.Store.Team().UpdateLastTeamIconUpdate(team.Id, curTime); result.Err != nil { return model.NewAppError("SetTeamIcon", "api.team.team_icon.update.app_error", nil, result.Err.Error(), http.StatusBadRequest) } diff --git a/app/user.go b/app/user.go index 05d0fe40f5..5021193270 100644 --- a/app/user.go +++ b/app/user.go @@ -801,10 +801,10 @@ func (a *App) SetProfileImage(userId string, imageData *multipart.FileHeader) *m return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, err.Error(), http.StatusBadRequest) } defer file.Close() - return a.SetProfileImageFromFile(userId, file) + return a.SetProfileImageFromMultiPartFile(userId, file) } -func (a *App) SetProfileImageFromFile(userId string, file multipart.File) *model.AppError { +func (a *App) SetProfileImageFromMultiPartFile(userId string, file multipart.File) *model.AppError { // Decode image config first to check dimensions before loading the whole thing into memory later on config, _, err := image.DecodeConfig(file) if err != nil { @@ -816,14 +816,17 @@ func (a *App) SetProfileImageFromFile(userId string, file multipart.File) *model file.Seek(0, 0) + return a.SetProfileImageFromFile(userId, file) +} + +func (a *App) SetProfileImageFromFile(userId string, file io.Reader) *model.AppError { + // Decode image into Image object img, _, err := image.Decode(file) if err != nil { return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error(), http.StatusBadRequest) } - file.Seek(0, 0) - orientation, _ := getImageOrientation(file) img = makeImageUpright(img, orientation) diff --git a/plugin/api.go b/plugin/api.go index 33d07c40ac..0aa8c6771a 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -64,6 +64,11 @@ type API interface { // Minimum server version: 5.6 GetUsersInTeam(teamId string, page int, perPage int) ([]*model.User, *model.AppError) + // GetTeamIcon gets the Team Icon. + // + // Minimum server version: 5.6 + GetTeamIcon(teamId string) ([]byte, *model.AppError) + // UpdateUser updates a user. UpdateUser(user *model.User) (*model.User, *model.AppError) @@ -264,6 +269,11 @@ type API interface { // Minimum server version: 5.6 GetProfileImage(userId string) ([]byte, *model.AppError) + // SetProfileImage sets a user's profile image. + // + // Minimum server version: 5.6 + SetProfileImage(userId string, data []byte) *model.AppError + // GetEmojiList returns a page of custom emoji on the system. // // The sortBy parameter can be: "name". diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 2e18934715..46b0c159fc 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -887,6 +887,35 @@ func (s *apiRPCServer) GetUsersInTeam(args *Z_GetUsersInTeamArgs, returns *Z_Get return nil } +type Z_GetTeamIconArgs struct { + A string +} + +type Z_GetTeamIconReturns struct { + A []byte + B *model.AppError +} + +func (g *apiRPCClient) GetTeamIcon(teamId string) ([]byte, *model.AppError) { + _args := &Z_GetTeamIconArgs{teamId} + _returns := &Z_GetTeamIconReturns{} + if err := g.client.Call("Plugin.GetTeamIcon", _args, _returns); err != nil { + log.Printf("RPC call to GetTeamIcon API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetTeamIcon(args *Z_GetTeamIconArgs, returns *Z_GetTeamIconReturns) error { + if hook, ok := s.impl.(interface { + GetTeamIcon(teamId string) ([]byte, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetTeamIcon(args.A) + } else { + return encodableError(fmt.Errorf("API GetTeamIcon called but not implemented.")) + } + return nil +} + type Z_UpdateUserArgs struct { A *model.User } @@ -2461,6 +2490,35 @@ func (s *apiRPCServer) GetProfileImage(args *Z_GetProfileImageArgs, returns *Z_G return nil } +type Z_SetProfileImageArgs struct { + A string + B []byte +} + +type Z_SetProfileImageReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) SetProfileImage(userId string, data []byte) *model.AppError { + _args := &Z_SetProfileImageArgs{userId, data} + _returns := &Z_SetProfileImageReturns{} + if err := g.client.Call("Plugin.SetProfileImage", _args, _returns); err != nil { + log.Printf("RPC call to SetProfileImage API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) SetProfileImage(args *Z_SetProfileImageArgs, returns *Z_SetProfileImageReturns) error { + if hook, ok := s.impl.(interface { + SetProfileImage(userId string, data []byte) *model.AppError + }); ok { + returns.A = hook.SetProfileImage(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API SetProfileImage called but not implemented.")) + } + return nil +} + type Z_GetEmojiListArgs struct { A string B int @@ -2756,35 +2814,6 @@ func (s *apiRPCServer) GetPlugins(args *Z_GetPluginsArgs, returns *Z_GetPluginsR return nil } -type Z_GetPluginStatusArgs struct { - A string -} - -type Z_GetPluginStatusReturns struct { - A *model.PluginStatus - B *model.AppError -} - -func (g *apiRPCClient) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { - _args := &Z_GetPluginStatusArgs{id} - _returns := &Z_GetPluginStatusReturns{} - if err := g.client.Call("Plugin.GetPluginStatus", _args, _returns); err != nil { - log.Printf("RPC call to GetPluginStatus API failed: %s", err.Error()) - } - return _returns.A, _returns.B -} - -func (s *apiRPCServer) GetPluginStatus(args *Z_GetPluginStatusArgs, returns *Z_GetPluginStatusReturns) error { - if hook, ok := s.impl.(interface { - GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) - }); ok { - returns.A, returns.B = hook.GetPluginStatus(args.A) - } else { - return encodableError(fmt.Errorf("API GetPluginStatus called but not implemented.")) - } - return nil -} - type Z_EnablePluginArgs struct { A string } @@ -2869,6 +2898,35 @@ func (s *apiRPCServer) RemovePlugin(args *Z_RemovePluginArgs, returns *Z_RemoveP return nil } +type Z_GetPluginStatusArgs struct { + A string +} + +type Z_GetPluginStatusReturns struct { + A *model.PluginStatus + B *model.AppError +} + +func (g *apiRPCClient) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { + _args := &Z_GetPluginStatusArgs{id} + _returns := &Z_GetPluginStatusReturns{} + if err := g.client.Call("Plugin.GetPluginStatus", _args, _returns); err != nil { + log.Printf("RPC call to GetPluginStatus API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetPluginStatus(args *Z_GetPluginStatusArgs, returns *Z_GetPluginStatusReturns) error { + if hook, ok := s.impl.(interface { + GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetPluginStatus(args.A) + } else { + return encodableError(fmt.Errorf("API GetPluginStatus called but not implemented.")) + } + return nil +} + type Z_KVSetArgs struct { A string B []byte diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index d192fe713f..2100da958c 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -1175,6 +1175,31 @@ func (_m *API) GetTeamByName(name string) (*model.Team, *model.AppError) { return r0, r1 } +// GetTeamIcon provides a mock function with given fields: teamId +func (_m *API) GetTeamIcon(teamId string) ([]byte, *model.AppError) { + ret := _m.Called(teamId) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(teamId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(teamId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetTeamMember provides a mock function with given fields: teamId, userId func (_m *API) GetTeamMember(teamId string, userId string) (*model.TeamMember, *model.AppError) { ret := _m.Called(teamId, userId) @@ -1835,6 +1860,22 @@ func (_m *API) SendEphemeralPost(userId string, post *model.Post) *model.Post { return r0 } +// SetProfileImage provides a mock function with given fields: userId, data +func (_m *API) SetProfileImage(userId string, data []byte) *model.AppError { + ret := _m.Called(userId, data) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok { + r0 = rf(userId, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // UnregisterCommand provides a mock function with given fields: teamId, trigger func (_m *API) UnregisterCommand(teamId string, trigger string) error { ret := _m.Called(teamId, trigger)