1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2// See LICENSE.txt for license information.
3
4package app
5
6import (
7	"bytes"
8	"context"
9	"encoding/json"
10	"errors"
11	"fmt"
12	"image"
13	"image/color/palette"
14	"image/draw"
15	"image/gif"
16	_ "image/jpeg"
17	"image/png"
18	"io"
19	"mime/multipart"
20	"net/http"
21	"path"
22
23	"github.com/disintegration/imaging"
24
25	"github.com/mattermost/mattermost-server/v6/model"
26	"github.com/mattermost/mattermost-server/v6/shared/mlog"
27	"github.com/mattermost/mattermost-server/v6/store"
28	"github.com/mattermost/mattermost-server/v6/utils"
29)
30
31const (
32	MaxEmojiFileSize       = 1 << 20 // 1 MB
33	MaxEmojiWidth          = 128
34	MaxEmojiHeight         = 128
35	MaxEmojiOriginalWidth  = 1028
36	MaxEmojiOriginalHeight = 1028
37)
38
39func (a *App) CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
40	if !*a.Config().ServiceSettings.EnableCustomEmoji {
41		return nil, model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
42	}
43
44	if *a.Config().FileSettings.DriverName == "" {
45		return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
46	}
47
48	// wipe the emoji id so that existing emojis can't get overwritten
49	emoji.Id = ""
50
51	// do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
52	// orphaned files left over when validation fails later on
53	emoji.PreSave()
54	if err := emoji.IsValid(); err != nil {
55		return nil, err
56	}
57
58	if emoji.CreatorId != sessionUserId {
59		return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden)
60	}
61
62	if existingEmoji, err := a.Srv().Store.Emoji().GetByName(context.Background(), emoji.Name, true); err == nil && existingEmoji != nil {
63		return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest)
64	}
65
66	imageData := multiPartImageData.File["image"]
67	if len(imageData) == 0 {
68		err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": "createEmoji"}, "", http.StatusBadRequest)
69		return nil, err
70	}
71
72	if err := a.UploadEmojiImage(emoji.Id, imageData[0]); err != nil {
73		return nil, err
74	}
75
76	emoji, err := a.Srv().Store.Emoji().Save(emoji)
77	if err != nil {
78		return nil, model.NewAppError("CreateEmoji", "app.emoji.create.internal_error", nil, err.Error(), http.StatusInternalServerError)
79	}
80
81	message := model.NewWebSocketEvent(model.WebsocketEventEmojiAdded, "", "", "", nil)
82	emojiJSON, jsonErr := json.Marshal(emoji)
83	if jsonErr != nil {
84		mlog.Warn("Failed to encode emoji to JSON", mlog.Err(jsonErr))
85	}
86	message.Add("emoji", string(emojiJSON))
87	a.Publish(message)
88	return emoji, nil
89}
90
91func (a *App) GetEmojiList(page, perPage int, sort string) ([]*model.Emoji, *model.AppError) {
92	list, err := a.Srv().Store.Emoji().GetList(page*perPage, perPage, sort)
93	if err != nil {
94		return nil, model.NewAppError("GetEmojiList", "app.emoji.get_list.internal_error", nil, err.Error(), http.StatusInternalServerError)
95	}
96
97	return list, nil
98}
99
100func (a *App) UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError {
101	if !*a.Config().ServiceSettings.EnableCustomEmoji {
102		return model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
103	}
104
105	if *a.Config().FileSettings.DriverName == "" {
106		return model.NewAppError("UploadEmojiImage", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
107	}
108
109	file, err := imageData.Open()
110	if err != nil {
111		return model.NewAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, err.Error(), http.StatusBadRequest)
112	}
113	defer file.Close()
114
115	buf := bytes.NewBuffer(nil)
116	io.Copy(buf, file)
117
118	// make sure the file is an image and is within the required dimensions
119	config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes()))
120	if err != nil {
121		return model.NewAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, err.Error(), http.StatusBadRequest)
122	}
123
124	if config.Width > MaxEmojiOriginalWidth || config.Height > MaxEmojiOriginalHeight {
125		return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.too_large.app_error", map[string]interface{}{
126			"MaxWidth":  MaxEmojiOriginalWidth,
127			"MaxHeight": MaxEmojiOriginalHeight,
128		}, "", http.StatusBadRequest)
129	}
130
131	if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
132		data := buf.Bytes()
133		newbuf := bytes.NewBuffer(nil)
134		info, err := model.GetInfoForBytes(imageData.Filename, bytes.NewReader(data), len(data))
135		if err != nil {
136			return err
137		}
138
139		if info.MimeType == "image/gif" {
140			gif_data, err := gif.DecodeAll(bytes.NewReader(data))
141			if err != nil {
142				return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, err.Error(), http.StatusBadRequest)
143			}
144
145			resized_gif := resizeEmojiGif(gif_data)
146			if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
147				return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, err.Error(), http.StatusBadRequest)
148			}
149
150			buf = newbuf
151		} else {
152			img, _, err := image.Decode(bytes.NewReader(data))
153			if err != nil {
154				return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, err.Error(), http.StatusBadRequest)
155			}
156
157			resized_image := resizeEmoji(img, config.Width, config.Height)
158			if err := png.Encode(newbuf, resized_image); err != nil {
159				return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, err.Error(), http.StatusBadRequest)
160			}
161			buf = newbuf
162		}
163	}
164
165	_, appErr := a.WriteFile(buf, getEmojiImagePath(id))
166	return appErr
167}
168
169func (a *App) DeleteEmoji(emoji *model.Emoji) *model.AppError {
170	if err := a.Srv().Store.Emoji().Delete(emoji, model.GetMillis()); err != nil {
171		var nfErr *store.ErrNotFound
172		switch {
173		case errors.As(err, &nfErr):
174			return model.NewAppError("DeleteEmoji", "app.emoji.delete.no_results", nil, "id="+emoji.Id+", err="+err.Error(), http.StatusNotFound)
175		default:
176			return model.NewAppError("DeleteEmoji", "app.emoji.delete.app_error", nil, "id="+emoji.Id+", err="+err.Error(), http.StatusInternalServerError)
177		}
178	}
179
180	a.deleteEmojiImage(emoji.Id)
181	a.deleteReactionsForEmoji(emoji.Name)
182	return nil
183}
184
185func (a *App) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
186	if !*a.Config().ServiceSettings.EnableCustomEmoji {
187		return nil, model.NewAppError("GetEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
188	}
189
190	if *a.Config().FileSettings.DriverName == "" {
191		return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
192	}
193
194	emoji, err := a.Srv().Store.Emoji().Get(context.Background(), emojiId, true)
195	if err != nil {
196		var nfErr *store.ErrNotFound
197		switch {
198		case errors.As(err, &nfErr):
199			return emoji, model.NewAppError("GetEmoji", "app.emoji.get.no_result", nil, err.Error(), http.StatusNotFound)
200		default:
201			return emoji, model.NewAppError("GetEmoji", "app.emoji.get.app_error", nil, err.Error(), http.StatusInternalServerError)
202		}
203	}
204
205	return emoji, nil
206}
207
208func (a *App) GetEmojiByName(emojiName string) (*model.Emoji, *model.AppError) {
209	if !*a.Config().ServiceSettings.EnableCustomEmoji {
210		return nil, model.NewAppError("GetEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
211	}
212
213	if *a.Config().FileSettings.DriverName == "" {
214		return nil, model.NewAppError("GetEmojiByName", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
215	}
216
217	emoji, err := a.Srv().Store.Emoji().GetByName(context.Background(), emojiName, true)
218	if err != nil {
219		var nfErr *store.ErrNotFound
220		switch {
221		case errors.As(err, &nfErr):
222			return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.no_result", nil, err.Error(), http.StatusNotFound)
223		default:
224			return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
225		}
226	}
227
228	return emoji, nil
229}
230
231func (a *App) GetMultipleEmojiByName(names []string) ([]*model.Emoji, *model.AppError) {
232	if !*a.Config().ServiceSettings.EnableCustomEmoji {
233		return nil, model.NewAppError("GetMultipleEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
234	}
235
236	emoji, err := a.Srv().Store.Emoji().GetMultipleByName(names)
237	if err != nil {
238		return nil, model.NewAppError("GetMultipleEmojiByName", "app.emoji.get_by_name.app_error", nil, fmt.Sprintf("names=%v, %v", names, err.Error()), http.StatusInternalServerError)
239	}
240
241	return emoji, nil
242}
243
244func (a *App) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) {
245	_, storeErr := a.Srv().Store.Emoji().Get(context.Background(), emojiId, true)
246	if storeErr != nil {
247		var nfErr *store.ErrNotFound
248		switch {
249		case errors.As(storeErr, &nfErr):
250			return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.no_result", nil, storeErr.Error(), http.StatusNotFound)
251		default:
252			return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
253		}
254	}
255
256	img, appErr := a.ReadFile(getEmojiImagePath(emojiId))
257	if appErr != nil {
258		return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, appErr.Error(), http.StatusNotFound)
259	}
260
261	_, imageType, err := image.DecodeConfig(bytes.NewReader(img))
262	if err != nil {
263		return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.decode.app_error", nil, err.Error(), http.StatusInternalServerError)
264	}
265
266	return img, imageType, nil
267}
268
269func (a *App) SearchEmoji(name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) {
270	if !*a.Config().ServiceSettings.EnableCustomEmoji {
271		return nil, model.NewAppError("SearchEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
272	}
273
274	list, err := a.Srv().Store.Emoji().Search(name, prefixOnly, limit)
275	if err != nil {
276		return nil, model.NewAppError("SearchEmoji", "app.emoji.get_by_name.app_error", nil, "name="+name+", "+err.Error(), http.StatusInternalServerError)
277	}
278
279	return list, nil
280}
281
282// GetEmojiStaticURL returns a relative static URL for system default emojis,
283// and the API route for custom ones. Errors if not found or if custom and deleted.
284func (a *App) GetEmojiStaticURL(emojiName string) (string, *model.AppError) {
285	subPath, _ := utils.GetSubpathFromConfig(a.Config())
286
287	if id, found := model.GetSystemEmojiId(emojiName); found {
288		return path.Join(subPath, "/static/emoji", id+".png"), nil
289	}
290
291	emoji, err := a.Srv().Store.Emoji().GetByName(context.Background(), emojiName, true)
292	if err == nil {
293		return path.Join(subPath, "/api/v4/emoji", emoji.Id, "image"), nil
294	}
295	var nfErr *store.ErrNotFound
296	switch {
297	case errors.As(err, &nfErr):
298		return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.no_result", nil, err.Error(), http.StatusNotFound)
299	default:
300		return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
301	}
302}
303
304func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
305	// Create a new RGBA image to hold the incremental frames.
306	firstFrame := gifImg.Image[0].Bounds()
307	b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
308	img := image.NewRGBA(b)
309
310	resizedImage := image.Image(nil)
311	// Resize each frame.
312	for index, frame := range gifImg.Image {
313		bounds := frame.Bounds()
314		draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
315		resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy())
316		gifImg.Image[index] = imageToPaletted(resizedImage)
317	}
318	// Set new gif width and height
319	gifImg.Config.Width = resizedImage.Bounds().Dx()
320	gifImg.Config.Height = resizedImage.Bounds().Dy()
321	return gifImg
322}
323
324func getEmojiImagePath(id string) string {
325	return "emoji/" + id + "/image"
326}
327
328func resizeEmoji(img image.Image, width int, height int) image.Image {
329	emojiWidth := float64(width)
330	emojiHeight := float64(height)
331
332	if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
333		return img
334	}
335	return imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
336}
337
338func imageToPaletted(img image.Image) *image.Paletted {
339	b := img.Bounds()
340	pm := image.NewPaletted(b, palette.Plan9)
341	draw.FloydSteinberg.Draw(pm, b, img, image.Point{})
342	return pm
343}
344
345func (a *App) deleteEmojiImage(id string) {
346	if err := a.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
347		mlog.Warn("Failed to rename image when deleting emoji", mlog.String("emoji_id", id))
348	}
349}
350
351func (a *App) deleteReactionsForEmoji(emojiName string) {
352	if err := a.Srv().Store.Reaction().DeleteAllWithEmojiName(emojiName); err != nil {
353		mlog.Warn("Unable to delete reactions when deleting emoji", mlog.String("emoji_name", emojiName), mlog.Err(err))
354	}
355}
356