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