diff options
Diffstat (limited to 'api4')
-rw-r--r-- | api4/api.go | 1 | ||||
-rw-r--r-- | api4/apitestlib.go | 68 | ||||
-rw-r--r-- | api4/context.go | 18 | ||||
-rw-r--r-- | api4/file.go | 114 | ||||
-rw-r--r-- | api4/file_test.go | 151 |
5 files changed, 348 insertions, 4 deletions
diff --git a/api4/api.go b/api4/api.go index b352d0b15..df45ff1a3 100644 --- a/api4/api.go +++ b/api4/api.go @@ -143,6 +143,7 @@ func InitApi(full bool) { InitTeam() InitChannel() InitPost() + InitFile() InitSystem() app.Srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 08ca338c5..27bf83f10 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -4,7 +4,10 @@ package api4 import ( + "bytes" + "io" "net/http" + "os" "reflect" "runtime/debug" "strconv" @@ -17,6 +20,8 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + + s3 "github.com/minio/minio-go" ) type TestHelper struct { @@ -398,3 +403,66 @@ func CheckErrorMessage(t *testing.T, resp *model.Response, errorId string) { t.Fatal("incorrect error message") } } + +func readTestFile(name string) ([]byte, error) { + path := utils.FindDir("tests") + file, err := os.Open(path + "/" + name) + if err != nil { + return nil, err + } + defer file.Close() + + data := &bytes.Buffer{} + if _, err := io.Copy(data, file); err != nil { + return nil, err + } else { + return data.Bytes(), nil + } +} + +func cleanupTestFile(info *model.FileInfo) error { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint + accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId + secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey + secure := *utils.Cfg.FileSettings.AmazonS3SSL + s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure) + if err != nil { + return err + } + bucket := utils.Cfg.FileSettings.AmazonS3Bucket + if err := s3Clnt.RemoveObject(bucket, info.Path); err != nil { + return err + } + + if info.ThumbnailPath != "" { + if err := s3Clnt.RemoveObject(bucket, info.ThumbnailPath); err != nil { + return err + } + } + + if info.PreviewPath != "" { + if err := s3Clnt.RemoveObject(bucket, info.PreviewPath); err != nil { + return err + } + } + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.Remove(utils.Cfg.FileSettings.Directory + info.Path); err != nil { + return err + } + + if info.ThumbnailPath != "" { + if err := os.Remove(utils.Cfg.FileSettings.Directory + info.ThumbnailPath); err != nil { + return err + } + } + + if info.PreviewPath != "" { + if err := os.Remove(utils.Cfg.FileSettings.Directory + info.PreviewPath); err != nil { + return err + } + } + } + + return nil +} diff --git a/api4/context.go b/api4/context.go index 6a844795f..d272e8049 100644 --- a/api4/context.go +++ b/api4/context.go @@ -382,12 +382,24 @@ func (c *Context) RequirePostId() *Context { return c } +func (c *Context) RequireFileId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.FileId) != 26 { + c.SetInvalidUrlParam("file_id") + } + + return c +} + func (c *Context) RequireTeamName() *Context { if c.Err != nil { return c } - if !model.IsValidTeamName(c.Params.TeamName){ + if !model.IsValidTeamName(c.Params.TeamName) { c.SetInvalidUrlParam("team_name") } @@ -401,7 +413,7 @@ func (c *Context) RequireChannelName() *Context { if !model.IsValidChannelIdentifier(c.Params.ChannelName) { c.SetInvalidUrlParam("channel_name") - } + } return c } @@ -417,5 +429,3 @@ func (c *Context) RequireEmail() *Context { return c } - - diff --git a/api4/file.go b/api4/file.go new file mode 100644 index 000000000..b486fc220 --- /dev/null +++ b/api4/file.go @@ -0,0 +1,114 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" + "net/url" + "strconv" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +const ( + FILE_TEAM_ID = "noteam" +) + +func InitFile() { + l4g.Debug(utils.T("api.file.init.debug")) + + BaseRoutes.Files.Handle("", ApiSessionRequired(uploadFile)).Methods("POST") + BaseRoutes.File.Handle("", ApiSessionRequired(getFile)).Methods("GET") + +} + +func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { + if r.ContentLength > *utils.Cfg.FileSettings.MaxFileSize { + c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "") + c.Err.StatusCode = http.StatusRequestEntityTooLarge + return + } + + if err := r.ParseMultipartForm(*utils.Cfg.FileSettings.MaxFileSize); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + m := r.MultipartForm + + props := m.Value + if len(props["channel_id"]) == 0 { + c.SetInvalidParam("channel_id") + return + } + channelId := props["channel_id"][0] + if len(channelId) == 0 { + c.SetInvalidParam("channel_id") + return + } + + if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) { + c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) + return + } + + resStruct, err := app.UploadFiles(FILE_TEAM_ID, channelId, c.Session.UserId, m.File["files"], m.Value["client_ids"]) + if err != nil { + c.Err = err + return + } + + w.WriteHeader(http.StatusCreated) + w.Write([]byte(resStruct.ToJson())) +} + +func getFile(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireFileId() + if c.Err != nil { + return + } + + info, err := app.GetFileInfo(c.Params.FileId) + if err != nil { + c.Err = err + return + } + + if info.CreatorId != c.Session.UserId && !app.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + if data, err := app.ReadFile(info.Path); err != nil { + c.Err = err + c.Err.StatusCode = http.StatusNotFound + } else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil { + c.Err = err + return + } +} + +func writeFileResponse(filename string, contentType string, bytes []byte, w http.ResponseWriter, r *http.Request) *model.AppError { + w.Header().Set("Cache-Control", "max-age=2592000, public") + w.Header().Set("Content-Length", strconv.Itoa(len(bytes))) + + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } else { + w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer + } + + w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+url.QueryEscape(filename)) + + // prevent file links from being embedded in iframes + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'") + + w.Write(bytes) + + return nil +} diff --git a/api4/file_test.go b/api4/file_test.go new file mode 100644 index 000000000..d47dd6cb1 --- /dev/null +++ b/api4/file_test.go @@ -0,0 +1,151 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "fmt" + "testing" + "time" + + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func TestUploadFile(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + user := th.BasicUser + channel := th.BasicChannel + + var uploadInfo *model.FileInfo + var data []byte + var err error + if data, err = readTestFile("test.png"); err != nil { + t.Fatal(err) + } else if fileResp, resp := Client.UploadFile(data, channel.Id, "test.png"); resp.Error != nil { + t.Fatal(resp.Error) + } else if len(fileResp.FileInfos) != 1 { + t.Fatal("should've returned a single file infos") + } else { + uploadInfo = fileResp.FileInfos[0] + } + + // The returned file info from the upload call will be missing some fields that will be stored in the database + if uploadInfo.CreatorId != user.Id { + t.Fatal("file should be assigned to user") + } else if uploadInfo.PostId != "" { + t.Fatal("file shouldn't have a post") + } else if uploadInfo.Path != "" { + t.Fatal("file path should not be set on returned info") + } else if uploadInfo.ThumbnailPath != "" { + t.Fatal("file thumbnail path should not be set on returned info") + } else if uploadInfo.PreviewPath != "" { + t.Fatal("file preview path should not be set on returned info") + } + + var info *model.FileInfo + if result := <-app.Srv.Store.FileInfo().Get(uploadInfo.Id); result.Err != nil { + t.Fatal(result.Err) + } else { + info = result.Data.(*model.FileInfo) + } + + if info.Id != uploadInfo.Id { + t.Fatal("file id from response should match one stored in database") + } else if info.CreatorId != user.Id { + t.Fatal("file should be assigned to user") + } else if info.PostId != "" { + t.Fatal("file shouldn't have a post") + } else if info.Path == "" { + t.Fatal("file path should be set in database") + } else if info.ThumbnailPath == "" { + t.Fatal("file thumbnail path should be set in database") + } else if info.PreviewPath == "" { + t.Fatal("file preview path should be set in database") + } + + // This also makes sure that the relative path provided above is sanitized out + expectedPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test.png", FILE_TEAM_ID, channel.Id, user.Id, info.Id) + if info.Path != expectedPath { + t.Logf("file is saved in %v", info.Path) + t.Fatalf("file should've been saved in %v", expectedPath) + } + + expectedThumbnailPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test_thumb.jpg", FILE_TEAM_ID, channel.Id, user.Id, info.Id) + if info.ThumbnailPath != expectedThumbnailPath { + t.Logf("file thumbnail is saved in %v", info.ThumbnailPath) + t.Fatalf("file thumbnail should've been saved in %v", expectedThumbnailPath) + } + + expectedPreviewPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test_preview.jpg", FILE_TEAM_ID, channel.Id, user.Id, info.Id) + if info.PreviewPath != expectedPreviewPath { + t.Logf("file preview is saved in %v", info.PreviewPath) + t.Fatalf("file preview should've been saved in %v", expectedPreviewPath) + } + + // Wait a bit for files to ready + time.Sleep(2 * time.Second) + + if err := cleanupTestFile(info); err != nil { + t.Fatal(err) + } + + _, resp := Client.UploadFile(data, model.NewId(), "test.png") + CheckForbiddenStatus(t, resp) + + _, resp = th.SystemAdminClient.UploadFile(data, channel.Id, "test.png") + CheckNoError(t, resp) +} + +func TestGetFile(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + channel := th.BasicChannel + + if utils.Cfg.FileSettings.DriverName == "" { + t.Skip("skipping because no file driver is enabled") + } + + fileId := "" + var sent []byte + var err error + if sent, err = readTestFile("test.png"); err != nil { + t.Fatal(err) + } else { + fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png") + CheckNoError(t, resp) + + fileId = fileResp.FileInfos[0].Id + } + + data, resp := Client.GetFile(fileId) + CheckNoError(t, resp) + + if data == nil || len(data) == 0 { + t.Fatal("should not be empty") + } + + for i := range data { + if data[i] != sent[i] { + t.Fatal("received file didn't match sent one") + } + } + + _, resp = Client.GetFile("junk") + CheckBadRequestStatus(t, resp) + + _, resp = Client.GetFile(model.NewId()) + CheckNotFoundStatus(t, resp) + + Client.Logout() + _, resp = Client.GetFile(fileId) + CheckUnauthorizedStatus(t, resp) + + _, resp = th.SystemAdminClient.GetFile(fileId) + CheckNoError(t, resp) +} |