github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/emoji.go (about)

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