summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/emoji.go72
-rw-r--r--api/emoji_test.go101
-rw-r--r--api4/api.go1
-rw-r--r--api4/apitestlib.go15
-rw-r--r--api4/emoji.go72
-rw-r--r--api4/emoji_test.go139
-rw-r--r--app/emoji.go156
-rw-r--r--i18n/en.json4
-rw-r--r--model/client4.go49
-rw-r--r--utils/emoji.go62
10 files changed, 528 insertions, 143 deletions
diff --git a/api/emoji.go b/api/emoji.go
index 2f94fb0e0..267644fc0 100644
--- a/api/emoji.go
+++ b/api/emoji.go
@@ -8,10 +8,6 @@ import (
"image"
"image/draw"
"image/gif"
- _ "image/jpeg"
- "image/png"
- "io"
- "mime/multipart"
"net/http"
"strings"
@@ -25,12 +21,6 @@ import (
"image/color/palette"
)
-const (
- MaxEmojiFileSize = 1000 * 1024 // 1 MB
- MaxEmojiWidth = 128
- MaxEmojiHeight = 128
-)
-
func InitEmoji() {
l4g.Debug(utils.T("api.emoji.init.debug"))
@@ -76,13 +66,13 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if r.ContentLength > MaxEmojiFileSize {
+ if r.ContentLength > app.MaxEmojiFileSize {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "")
c.Err.StatusCode = http.StatusRequestEntityTooLarge
return
}
- if err := r.ParseMultipartForm(MaxEmojiFileSize); err != nil {
+ if err := r.ParseMultipartForm(app.MaxEmojiFileSize); err != nil {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error())
c.Err.StatusCode = http.StatusBadRequest
return
@@ -124,7 +114,7 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
if imageData := m.File["image"]; len(imageData) == 0 {
c.SetInvalidParam("createEmoji", "image")
return
- } else if err := uploadEmojiImage(emoji.Id, imageData[0]); err != nil {
+ } else if err := app.UploadEmojiImage(emoji.Id, imageData[0]); err != nil {
c.Err = err
return
}
@@ -137,58 +127,6 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError {
- file, err := imageData.Open()
- if err != nil {
- return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "")
- }
- defer file.Close()
-
- buf := bytes.NewBuffer(nil)
- io.Copy(buf, file)
-
- // make sure the file is an image and is within the required dimensions
- if config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes())); err != nil {
- return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, err.Error())
- } else if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
- data := buf.Bytes()
- newbuf := bytes.NewBuffer(nil)
- if info, err := model.GetInfoForBytes(imageData.Filename, data); err != nil {
- return err
- } else if info.MimeType == "image/gif" {
- if gif_data, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
- return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "")
- } else {
- resized_gif := resizeEmojiGif(gif_data)
- if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
- return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "")
- }
- if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
- return err
- }
- }
- } else {
- if img, _, err := image.Decode(bytes.NewReader(data)); err != nil {
- return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "")
- } else {
- resized_image := resizeEmoji(img, config.Width, config.Height)
- if err := png.Encode(newbuf, resized_image); err != nil {
- return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "")
- }
- if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
- return err
- }
- }
- }
- } else {
- if err := app.WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
- return err
- }
- }
-
- return nil
-}
-
func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.disabled.app_error", nil, "")
@@ -302,10 +240,10 @@ func resizeEmoji(img image.Image, width int, height int) image.Image {
emojiHeight := float64(height)
var emoji image.Image
- if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
+ if emojiHeight <= app.MaxEmojiHeight && emojiWidth <= app.MaxEmojiWidth {
emoji = img
} else {
- emoji = imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
+ emoji = imaging.Fit(img, app.MaxEmojiWidth, app.MaxEmojiHeight, imaging.Lanczos)
}
return emoji
}
diff --git a/api/emoji_test.go b/api/emoji_test.go
index fb90d8781..d8d624d51 100644
--- a/api/emoji_test.go
+++ b/api/emoji_test.go
@@ -6,10 +6,7 @@ package api
import (
"bytes"
"image"
- "image/color"
"image/gif"
- "image/jpeg"
- "image/png"
"testing"
"time"
@@ -113,14 +110,14 @@ func TestCreateEmoji(t *testing.T) {
}
// try to create an emoji when they're disabled
- if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
+ if _, err := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif"); err == nil {
t.Fatal("shouldn't be able to create an emoji when they're disabled")
}
*utils.Cfg.ServiceSettings.EnableCustomEmoji = true
// try to create a valid gif emoji when they're enabled
- if emojiResult, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err != nil {
+ if emojiResult, err := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif"); err != nil {
t.Fatal(err)
} else {
emoji = emojiResult
@@ -131,7 +128,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: emoji.Name,
}
- if _, err := Client.CreateEmoji(emoji2, createTestGif(t, 10, 10), "image.gif"); err == nil {
+ if _, err := Client.CreateEmoji(emoji2, utils.CreateTestGif(t, 10, 10), "image.gif"); err == nil {
t.Fatal("shouldn't be able to create an emoji with a duplicate name")
}
@@ -142,7 +139,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- if emojiResult, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 10, 10, 10), "image.gif"); err != nil {
+ if emojiResult, err := Client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, 10, 10, 10), "image.gif"); err != nil {
t.Fatal(err)
} else {
emoji = emojiResult
@@ -154,7 +151,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- if emojiResult, err := Client.CreateEmoji(emoji, createTestJpeg(t, 10, 10), "image.jpeg"); err != nil {
+ if emojiResult, err := Client.CreateEmoji(emoji, utils.CreateTestJpeg(t, 10, 10), "image.jpeg"); err != nil {
t.Fatal(err)
} else {
emoji = emojiResult
@@ -166,7 +163,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- if emojiResult, err := Client.CreateEmoji(emoji, createTestPng(t, 10, 10), "image.png"); err != nil {
+ if emojiResult, err := Client.CreateEmoji(emoji, utils.CreateTestPng(t, 10, 10), "image.png"); err != nil {
t.Fatal(err)
} else {
emoji = emojiResult
@@ -178,7 +175,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- if _, err := Client.CreateEmoji(emoji, createTestGif(t, 1000, 10), "image.gif"); err != nil {
+ if _, err := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 1000, 10), "image.gif"); err != nil {
t.Fatal("should be able to create an emoji that's too wide by resizing it")
}
@@ -187,7 +184,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 1000), "image.gif"); err != nil {
+ if _, err := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 1000), "image.gif"); err != nil {
t.Fatal("should be able to create an emoji that's too tall by resizing it")
}
@@ -196,7 +193,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- if _, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 100, 100, 10000), "image.gif"); err == nil {
+ if _, err := Client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, 100, 100, 10000), "image.gif"); err == nil {
t.Fatal("shouldn't be able to create an emoji that's too large")
}
@@ -214,7 +211,7 @@ func TestCreateEmoji(t *testing.T) {
CreatorId: th.BasicUser2.Id,
Name: model.NewId(),
}
- if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
+ if _, err := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif"); err == nil {
t.Fatal("shouldn't be able to create an emoji as another user")
}
}
@@ -232,7 +229,7 @@ func TestDeleteEmoji(t *testing.T) {
emoji1 := createTestEmoji(t, &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
- }, createTestGif(t, 10, 10))
+ }, utils.CreateTestGif(t, 10, 10))
if _, err := Client.DeleteEmoji(emoji1.Id); err == nil {
t.Fatal("shouldn't have been able to delete an emoji when they're disabled")
@@ -253,7 +250,7 @@ func TestDeleteEmoji(t *testing.T) {
emoji2 := createTestEmoji(t, &model.Emoji{
CreatorId: th.BasicUser2.Id,
Name: model.NewId(),
- }, createTestGif(t, 10, 10))
+ }, utils.CreateTestGif(t, 10, 10))
if _, err := Client.DeleteEmoji(emoji2.Id); err == nil {
t.Fatal("shouldn't be able to delete another user's emoji")
@@ -266,54 +263,6 @@ func TestDeleteEmoji(t *testing.T) {
}
}
-func createTestGif(t *testing.T, width int, height int) []byte {
- var buffer bytes.Buffer
-
- if err := gif.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil); err != nil {
- t.Fatalf("failed to create gif: %v", err.Error())
- }
-
- return buffer.Bytes()
-}
-
-func createTestAnimatedGif(t *testing.T, width int, height int, frames int) []byte {
- var buffer bytes.Buffer
-
- img := gif.GIF{
- Image: make([]*image.Paletted, frames, frames),
- Delay: make([]int, frames, frames),
- }
- for i := 0; i < frames; i++ {
- img.Image[i] = image.NewPaletted(image.Rect(0, 0, width, height), color.Palette{color.Black})
- img.Delay[i] = 0
- }
- if err := gif.EncodeAll(&buffer, &img); err != nil {
- t.Fatalf("failed to create animated gif: %v", err.Error())
- }
-
- return buffer.Bytes()
-}
-
-func createTestJpeg(t *testing.T, width int, height int) []byte {
- var buffer bytes.Buffer
-
- if err := jpeg.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil); err != nil {
- t.Fatalf("failed to create jpeg: %v", err.Error())
- }
-
- return buffer.Bytes()
-}
-
-func createTestPng(t *testing.T, width int, height int) []byte {
- var buffer bytes.Buffer
-
- if err := png.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height))); err != nil {
- t.Fatalf("failed to create png: %v", err.Error())
- }
-
- return buffer.Bytes()
-}
-
func createTestEmoji(t *testing.T, emoji *model.Emoji, imageData []byte) *model.Emoji {
emoji = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
@@ -342,7 +291,7 @@ func TestGetEmojiImage(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- emoji1 = Client.MustGeneric(Client.CreateEmoji(emoji1, createTestGif(t, 10, 10), "image.gif")).(*model.Emoji)
+ emoji1 = Client.MustGeneric(Client.CreateEmoji(emoji1, utils.CreateTestGif(t, 10, 10), "image.gif")).(*model.Emoji)
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji1.Id)) }()
*utils.Cfg.ServiceSettings.EnableCustomEmoji = false
@@ -367,7 +316,7 @@ func TestGetEmojiImage(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- emoji2 = Client.MustGeneric(Client.CreateEmoji(emoji2, createTestAnimatedGif(t, 10, 10, 10), "image.gif")).(*model.Emoji)
+ emoji2 = Client.MustGeneric(Client.CreateEmoji(emoji2, utils.CreateTestAnimatedGif(t, 10, 10, 10), "image.gif")).(*model.Emoji)
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji2.Id)) }()
if resp, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji2.Id), "", ""); err != nil {
@@ -384,7 +333,7 @@ func TestGetEmojiImage(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- emoji3 = Client.MustGeneric(Client.CreateEmoji(emoji3, createTestJpeg(t, 10, 10), "image.jpeg")).(*model.Emoji)
+ emoji3 = Client.MustGeneric(Client.CreateEmoji(emoji3, utils.CreateTestJpeg(t, 10, 10), "image.jpeg")).(*model.Emoji)
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji3.Id)) }()
if resp, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji3.Id), "", ""); err != nil {
@@ -401,7 +350,7 @@ func TestGetEmojiImage(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- emoji4 = Client.MustGeneric(Client.CreateEmoji(emoji4, createTestPng(t, 10, 10), "image.png")).(*model.Emoji)
+ emoji4 = Client.MustGeneric(Client.CreateEmoji(emoji4, utils.CreateTestPng(t, 10, 10), "image.png")).(*model.Emoji)
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji4.Id)) }()
if resp, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji4.Id), "", ""); err != nil {
@@ -418,7 +367,7 @@ func TestGetEmojiImage(t *testing.T) {
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
- emoji5 = Client.MustGeneric(Client.CreateEmoji(emoji5, createTestPng(t, 10, 10), "image.png")).(*model.Emoji)
+ emoji5 = Client.MustGeneric(Client.CreateEmoji(emoji5, utils.CreateTestPng(t, 10, 10), "image.png")).(*model.Emoji)
Client.MustGeneric(Client.DeleteEmoji(emoji5.Id))
if _, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji5.Id), "", ""); err == nil {
@@ -428,12 +377,12 @@ func TestGetEmojiImage(t *testing.T) {
func TestResizeEmoji(t *testing.T) {
// try to resize a jpeg image within MaxEmojiWidth and MaxEmojiHeight
- small_img_data := createTestJpeg(t, MaxEmojiWidth, MaxEmojiHeight)
+ small_img_data := utils.CreateTestJpeg(t, app.MaxEmojiWidth, app.MaxEmojiHeight)
if small_img, _, err := image.Decode(bytes.NewReader(small_img_data)); err != nil {
t.Fatal("failed to decode jpeg bytes to image.Image")
} else {
resized_img := resizeEmoji(small_img, small_img.Bounds().Dx(), small_img.Bounds().Dy())
- if resized_img.Bounds().Dx() > MaxEmojiWidth || resized_img.Bounds().Dy() > MaxEmojiHeight {
+ if resized_img.Bounds().Dx() > app.MaxEmojiWidth || resized_img.Bounds().Dy() > app.MaxEmojiHeight {
t.Fatal("resized jpeg width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
}
if resized_img != small_img {
@@ -441,32 +390,32 @@ func TestResizeEmoji(t *testing.T) {
}
}
// try to resize a jpeg image
- jpeg_data := createTestJpeg(t, 256, 256)
+ jpeg_data := utils.CreateTestJpeg(t, 256, 256)
if jpeg_img, _, err := image.Decode(bytes.NewReader(jpeg_data)); err != nil {
t.Fatal("failed to decode jpeg bytes to image.Image")
} else {
resized_jpeg := resizeEmoji(jpeg_img, jpeg_img.Bounds().Dx(), jpeg_img.Bounds().Dy())
- if resized_jpeg.Bounds().Dx() > MaxEmojiWidth || resized_jpeg.Bounds().Dy() > MaxEmojiHeight {
+ if resized_jpeg.Bounds().Dx() > app.MaxEmojiWidth || resized_jpeg.Bounds().Dy() > app.MaxEmojiHeight {
t.Fatal("resized jpeg width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
}
}
// try to resize a png image
- png_data := createTestJpeg(t, 256, 256)
+ png_data := utils.CreateTestJpeg(t, 256, 256)
if png_img, _, err := image.Decode(bytes.NewReader(png_data)); err != nil {
t.Fatal("failed to decode png bytes to image.Image")
} else {
resized_png := resizeEmoji(png_img, png_img.Bounds().Dx(), png_img.Bounds().Dy())
- if resized_png.Bounds().Dx() > MaxEmojiWidth || resized_png.Bounds().Dy() > MaxEmojiHeight {
+ if resized_png.Bounds().Dx() > app.MaxEmojiWidth || resized_png.Bounds().Dy() > app.MaxEmojiHeight {
t.Fatal("resized png width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
}
}
// try to resize an animated gif
- gif_data := createTestAnimatedGif(t, 256, 256, 10)
+ gif_data := utils.CreateTestAnimatedGif(t, 256, 256, 10)
if gif_img, err := gif.DecodeAll(bytes.NewReader(gif_data)); err != nil {
t.Fatal("failed to decode gif bytes to gif.GIF")
} else {
resized_gif := resizeEmojiGif(gif_img)
- if resized_gif.Config.Width > MaxEmojiWidth || resized_gif.Config.Height > MaxEmojiHeight {
+ if resized_gif.Config.Width > app.MaxEmojiWidth || resized_gif.Config.Height > app.MaxEmojiHeight {
t.Fatal("resized gif width and height should not be greater than MaxEmojiWidth or MaxEmojiHeight")
}
if len(resized_gif.Image) != len(gif_img.Image) {
diff --git a/api4/api.go b/api4/api.go
index ea46c8ee5..6451e4102 100644
--- a/api4/api.go
+++ b/api4/api.go
@@ -176,6 +176,7 @@ func InitApi(full bool) {
InitCommand()
InitStatus()
InitWebSocket()
+ InitEmoji()
app.Srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404))
diff --git a/api4/apitestlib.go b/api4/apitestlib.go
index bd36f49cc..8b5861026 100644
--- a/api4/apitestlib.go
+++ b/api4/apitestlib.go
@@ -564,6 +564,21 @@ func CheckInternalErrorStatus(t *testing.T, resp *model.Response) {
}
}
+func CheckPayLoadTooLargeStatus(t *testing.T, resp *model.Response) {
+ if resp.Error == nil {
+ debug.PrintStack()
+ t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusRequestEntityTooLarge))
+ return
+ }
+
+ if resp.StatusCode != http.StatusRequestEntityTooLarge {
+ debug.PrintStack()
+ 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/emoji.go b/api4/emoji.go
new file mode 100644
index 000000000..a32436b64
--- /dev/null
+++ b/api4/emoji.go
@@ -0,0 +1,72 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func InitEmoji() {
+ l4g.Debug(utils.T("api.emoji.init.debug"))
+
+ BaseRoutes.Emojis.Handle("", ApiSessionRequired(createEmoji)).Methods("POST")
+}
+
+func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
+ c.Err = model.NewLocAppError("createEmoji", "api.emoji.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ if emojiInterface := einterfaces.GetEmojiInterface(); emojiInterface != nil &&
+ !emojiInterface.CanUserCreateEmoji(c.Session.Roles, c.Session.TeamMembers) {
+ c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.permissions.app_error", nil, "user_id="+c.Session.UserId)
+ c.Err.StatusCode = http.StatusUnauthorized
+ return
+ }
+
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ c.Err = model.NewLocAppError("createEmoji", "api.emoji.storage.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ if r.ContentLength > app.MaxEmojiFileSize {
+ c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "")
+ c.Err.StatusCode = http.StatusRequestEntityTooLarge
+ return
+ }
+
+ if err := r.ParseMultipartForm(app.MaxEmojiFileSize); err != nil {
+ c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error())
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ m := r.MultipartForm
+ props := m.Value
+
+ emoji := model.EmojiFromJson(strings.NewReader(props["emoji"][0]))
+ if emoji == nil {
+ c.SetInvalidParam("createEmoji")
+ return
+ }
+
+ newEmoji, err := app.CreateEmoji(c.Session.UserId, emoji, m)
+ if err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Write([]byte(newEmoji.ToJson()))
+ }
+}
diff --git a/api4/emoji_test.go b/api4/emoji_test.go
new file mode 100644
index 000000000..9db231d4a
--- /dev/null
+++ b/api4/emoji_test.go
@@ -0,0 +1,139 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func TestCreateEmoji(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer TearDown()
+ Client := th.Client
+
+ EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
+ defer func() {
+ *utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
+ }()
+ *utils.Cfg.ServiceSettings.EnableCustomEmoji = false
+
+ emoji := &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ // try to create an emoji when they're disabled
+ _, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNotImplementedStatus(t, resp)
+
+ *utils.Cfg.ServiceSettings.EnableCustomEmoji = true
+ // try to create a valid gif emoji when they're enabled
+ newEmoji, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+ if newEmoji.Name != emoji.Name {
+ t.Fatal("create with wrong name")
+ }
+
+ // try to create an emoji with a duplicate name
+ emoji2 := &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: newEmoji.Name,
+ }
+ _, resp = Client.CreateEmoji(emoji2, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckBadRequestStatus(t, resp)
+ CheckErrorMessage(t, resp, "api.emoji.create.duplicate.app_error")
+
+ // try to create a valid animated gif emoji
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, 10, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+ if newEmoji.Name != emoji.Name {
+ t.Fatal("create with wrong name")
+ }
+
+ // try to create a valid jpeg emoji
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestJpeg(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+ if newEmoji.Name != emoji.Name {
+ t.Fatal("create with wrong name")
+ }
+
+ // try to create a valid png emoji
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestPng(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+ if newEmoji.Name != emoji.Name {
+ t.Fatal("create with wrong name")
+ }
+
+ // try to create an emoji that's too wide
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 1000, 10), "image.gif")
+ CheckNoError(t, resp)
+ if newEmoji.Name != emoji.Name {
+ t.Fatal("create with wrong name")
+ }
+
+ // try to create an emoji that's too tall
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 1000), "image.gif")
+ CheckNoError(t, resp)
+ if newEmoji.Name != emoji.Name {
+ t.Fatal("create with wrong name")
+ }
+
+ // try to create an emoji that's too large
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ _, resp = Client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, 100, 100, 10000), "image.gif")
+ if resp.Error == nil {
+ t.Fatal("should fail - emoji is too big")
+ }
+
+ // try to create an emoji with data that isn't an image
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser.Id,
+ Name: model.NewId(),
+ }
+
+ _, resp = Client.CreateEmoji(emoji, make([]byte, 100, 100), "image.gif")
+ CheckBadRequestStatus(t, resp)
+ CheckErrorMessage(t, resp, "api.emoji.upload.image.app_error")
+
+ // try to create an emoji as another user
+ emoji = &model.Emoji{
+ CreatorId: th.BasicUser2.Id,
+ Name: model.NewId(),
+ }
+
+ _, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckForbiddenStatus(t, resp)
+}
diff --git a/app/emoji.go b/app/emoji.go
new file mode 100644
index 000000000..0815b7e54
--- /dev/null
+++ b/app/emoji.go
@@ -0,0 +1,156 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bytes"
+ "image"
+ "image/color/palette"
+ "image/draw"
+ "image/gif"
+ _ "image/jpeg"
+ "image/png"
+ "io"
+ "mime/multipart"
+ "net/http"
+
+ "github.com/disintegration/imaging"
+ "github.com/mattermost/platform/model"
+)
+
+const (
+ MaxEmojiFileSize = 1000 * 1024 // 1 MB
+ MaxEmojiWidth = 128
+ MaxEmojiHeight = 128
+)
+
+func CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
+ // wipe the emoji id so that existing emojis can't get overwritten
+ emoji.Id = ""
+
+ // do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
+ // orphaned files left over when validation fails later on
+ emoji.PreSave()
+ if err := emoji.IsValid(); err != nil {
+ return nil, err
+ }
+
+ if emoji.CreatorId != sessionUserId {
+ return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden)
+ }
+
+ if result := <-Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil {
+ return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest)
+ }
+
+ if imageData := multiPartImageData.File["image"]; len(imageData) == 0 {
+ err := model.NewLocAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": "createEmoji"}, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ } else if err := UploadEmojiImage(emoji.Id, imageData[0]); err != nil {
+ return nil, err
+ }
+
+ if result := <-Srv.Store.Emoji().Save(emoji); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Emoji), nil
+ }
+}
+
+func UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError {
+ file, err := imageData.Open()
+ if err != nil {
+ return model.NewAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "", http.StatusBadRequest)
+ }
+ defer file.Close()
+
+ buf := bytes.NewBuffer(nil)
+ io.Copy(buf, file)
+
+ // make sure the file is an image and is within the required dimensions
+ if config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes())); err != nil {
+ return model.NewAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, "", http.StatusBadRequest)
+ } else if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
+ data := buf.Bytes()
+ newbuf := bytes.NewBuffer(nil)
+ if info, err := model.GetInfoForBytes(imageData.Filename, data); err != nil {
+ return err
+ } else if info.MimeType == "image/gif" {
+ if gif_data, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
+ return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "", http.StatusBadRequest)
+ } else {
+ resized_gif := resizeEmojiGif(gif_data)
+ if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
+ return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "", http.StatusBadRequest)
+ }
+ if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
+ return err
+ }
+ }
+ } else {
+ if img, _, err := image.Decode(bytes.NewReader(data)); err != nil {
+ return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "", http.StatusBadRequest)
+ } else {
+ resized_image := resizeEmoji(img, config.Width, config.Height)
+ if err := png.Encode(newbuf, resized_image); err != nil {
+ return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest)
+ }
+ if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
+ return err
+ }
+ }
+ }
+ } else {
+ if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
+ // Create a new RGBA image to hold the incremental frames.
+ firstFrame := gifImg.Image[0].Bounds()
+ b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
+ img := image.NewRGBA(b)
+
+ resizedImage := image.Image(nil)
+ // Resize each frame.
+ for index, frame := range gifImg.Image {
+ bounds := frame.Bounds()
+ draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
+ resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy())
+ gifImg.Image[index] = imageToPaletted(resizedImage)
+ }
+ // Set new gif width and height
+ gifImg.Config.Width = resizedImage.Bounds().Dx()
+ gifImg.Config.Height = resizedImage.Bounds().Dy()
+ return gifImg
+}
+
+func getEmojiImagePath(id string) string {
+ return "emoji/" + id + "/image"
+}
+
+func resizeEmoji(img image.Image, width int, height int) image.Image {
+ emojiWidth := float64(width)
+ emojiHeight := float64(height)
+
+ var emoji image.Image
+ if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
+ emoji = img
+ } else {
+ emoji = imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
+ }
+ return emoji
+}
+
+func imageToPaletted(img image.Image) *image.Paletted {
+ b := img.Bounds()
+ pm := image.NewPaletted(b, palette.Plan9)
+ draw.FloydSteinberg.Draw(pm, b, img, image.ZP)
+ return pm
+}
diff --git a/i18n/en.json b/i18n/en.json
index a04c11066..00ed3f2c2 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -444,6 +444,10 @@
"translation": "Initializing command API routes"
},
{
+ "id": "api.emoji.init.debug",
+ "translation": "Initializing emoji API routes"
+ },
+ {
"id": "api.command.invite_people.desc",
"translation": "Send an email invite to your Mattermost team"
},
diff --git a/model/client4.go b/model/client4.go
index 62c58eb00..9505fb79c 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -226,6 +226,10 @@ func (c *Client4) GetCommandsRoute() string {
return fmt.Sprintf("/commands")
}
+func (c *Client4) GetEmojisRoute() string {
+ return fmt.Sprintf("/emoji")
+}
+
func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodGet, url, "", etag)
}
@@ -285,6 +289,25 @@ func (c *Client4) DoUploadFile(url string, data []byte, contentType string) (*Fi
}
}
+func (c *Client4) DoEmojiUploadFile(url string, data []byte, contentType string) (*Emoji, *Response) {
+ rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data))
+ rq.Header.Set("Content-Type", contentType)
+ rq.Close = true
+
+ if len(c.AuthToken) > 0 {
+ rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
+ }
+
+ if rp, err := c.HttpClient.Do(rq); err != nil {
+ return nil, &Response{Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0)}
+ } else if rp.StatusCode >= 300 {
+ return nil, &Response{StatusCode: rp.StatusCode, Error: AppErrorFromJson(rp.Body)}
+ } else {
+ defer closeBody(rp)
+ return EmojiFromJson(rp.Body), BuildResponse(rp)
+ }
+}
+
func (c *Client4) DoUploadImportTeam(url string, data []byte, contentType string) ([]byte, *Response) {
rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
@@ -2218,3 +2241,29 @@ func (c *Client4) UpdateUserStatus(userId string, userStatus *Status) (*Status,
}
}
+
+// Emoji Section
+
+// CreateEmoji will save an emoji to the server if the current user has permission
+// to do so. If successful, the provided emoji will be returned with its Id field
+// filled in. Otherwise, an error will be returned.
+func (c *Client4) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoji, *Response) {
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+
+ if part, err := writer.CreateFormFile("image", filename); err != nil {
+ return nil, &Response{StatusCode: http.StatusForbidden, Error: NewLocAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error())}
+ } else if _, err = io.Copy(part, bytes.NewBuffer(image)); err != nil {
+ return nil, &Response{StatusCode: http.StatusForbidden, Error: NewLocAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error())}
+ }
+
+ if err := writer.WriteField("emoji", emoji.ToJson()); err != nil {
+ return nil, &Response{StatusCode: http.StatusForbidden, Error: NewLocAppError("CreateEmoji", "model.client.create_emoji.emoji.app_error", nil, err.Error())}
+ }
+
+ if err := writer.Close(); err != nil {
+ return nil, &Response{StatusCode: http.StatusForbidden, Error: NewLocAppError("CreateEmoji", "model.client.create_emoji.writer.app_error", nil, err.Error())}
+ }
+
+ return c.DoEmojiUploadFile(c.GetEmojisRoute(), body.Bytes(), writer.FormDataContentType())
+}
diff --git a/utils/emoji.go b/utils/emoji.go
new file mode 100644
index 000000000..86cabeb9c
--- /dev/null
+++ b/utils/emoji.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "bytes"
+ "image"
+ "image/color"
+ "image/gif"
+ "image/jpeg"
+ "image/png"
+ "testing"
+)
+
+func CreateTestGif(t *testing.T, width int, height int) []byte {
+ var buffer bytes.Buffer
+
+ if err := gif.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil); err != nil {
+ t.Fatalf("failed to create gif: %v", err.Error())
+ }
+
+ return buffer.Bytes()
+}
+
+func CreateTestAnimatedGif(t *testing.T, width int, height int, frames int) []byte {
+ var buffer bytes.Buffer
+
+ img := gif.GIF{
+ Image: make([]*image.Paletted, frames, frames),
+ Delay: make([]int, frames, frames),
+ }
+ for i := 0; i < frames; i++ {
+ img.Image[i] = image.NewPaletted(image.Rect(0, 0, width, height), color.Palette{color.Black})
+ img.Delay[i] = 0
+ }
+ if err := gif.EncodeAll(&buffer, &img); err != nil {
+ t.Fatalf("failed to create animated gif: %v", err.Error())
+ }
+
+ return buffer.Bytes()
+}
+
+func CreateTestJpeg(t *testing.T, width int, height int) []byte {
+ var buffer bytes.Buffer
+
+ if err := jpeg.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil); err != nil {
+ t.Fatalf("failed to create jpeg: %v", err.Error())
+ }
+
+ return buffer.Bytes()
+}
+
+func CreateTestPng(t *testing.T, width int, height int) []byte {
+ var buffer bytes.Buffer
+
+ if err := png.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height))); err != nil {
+ t.Fatalf("failed to create png: %v", err.Error())
+ }
+
+ return buffer.Bytes()
+}