* MM-38982: fixes SVG uploading
Uploading SVG files was "almost" treated like an image in some places,
resulting in not showing the SVG on the client.
This is happening because when we fail to generate a mini preview for an
image, we prepend "invalid-" to the MimeType.
This commit adds a check to guard against SVG files in methods that make
no sense for SVGs.
* Reverts invalid-{mimetype} fix
* Uses fileInfo.IsSvg() where it can
316 строки
10 KiB
Go
316 строки
10 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/app/request"
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
"github.com/mattermost/mattermost-server/v6/plugin"
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
"github.com/mattermost/mattermost-server/v6/store"
|
|
)
|
|
|
|
const minFirstPartSize = 5 * 1024 * 1024 // 5MB
|
|
|
|
func (a *App) runPluginsHook(c *request.Context, info *model.FileInfo, file io.Reader) *model.AppError {
|
|
pluginsEnvironment := a.GetPluginsEnvironment()
|
|
if pluginsEnvironment == nil {
|
|
return nil
|
|
}
|
|
|
|
filePath := info.Path
|
|
// using a pipe to avoid loading the whole file content in memory.
|
|
r, w := io.Pipe()
|
|
errChan := make(chan *model.AppError, 1)
|
|
hookHasRunCh := make(chan struct{})
|
|
|
|
go func() {
|
|
defer w.Close()
|
|
defer close(hookHasRunCh)
|
|
defer close(errChan)
|
|
var rejErr *model.AppError
|
|
var once sync.Once
|
|
pluginContext := pluginContext(c)
|
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
|
once.Do(func() {
|
|
hookHasRunCh <- struct{}{}
|
|
})
|
|
newInfo, rejStr := hooks.FileWillBeUploaded(pluginContext, info, file, w)
|
|
if rejStr != "" {
|
|
rejErr = model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.rejected",
|
|
map[string]interface{}{"Filename": info.Name, "Reason": rejStr}, "", http.StatusBadRequest)
|
|
return false
|
|
}
|
|
if newInfo != nil {
|
|
info = newInfo
|
|
}
|
|
return true
|
|
}, plugin.FileWillBeUploadedID)
|
|
if rejErr != nil {
|
|
errChan <- rejErr
|
|
}
|
|
}()
|
|
|
|
// If the plugin hook has not run we can return early.
|
|
if _, ok := <-hookHasRunCh; !ok {
|
|
return nil
|
|
}
|
|
|
|
tmpPath := filePath + ".tmp"
|
|
written, err := a.WriteFile(r, tmpPath)
|
|
if err != nil {
|
|
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err = <-errChan; err != nil {
|
|
if fileErr := a.RemoveFile(info.Path); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
return err
|
|
}
|
|
|
|
if written > 0 {
|
|
info.Size = written
|
|
if fileErr := a.MoveFile(tmpPath, info.Path); fileErr != nil {
|
|
return model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.move_fail",
|
|
nil, fileErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
} else {
|
|
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, *model.AppError) {
|
|
if us.FileSize > *a.Config().FileSettings.MaxFileSize {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.upload_too_large.app_error",
|
|
map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
|
|
}
|
|
|
|
us.FileOffset = 0
|
|
now := time.Now()
|
|
us.CreateAt = model.GetMillisForTime(now)
|
|
if us.Type == model.UploadTypeAttachment {
|
|
us.Path = now.Format("20060102") + "/teams/noteam/channels/" + us.ChannelId + "/users/" + us.UserId + "/" + us.Id + "/" + filepath.Base(us.Filename)
|
|
} else if us.Type == model.UploadTypeImport {
|
|
us.Path = filepath.Clean(*a.Config().ImportSettings.Directory) + "/" + us.Id + "_" + filepath.Base(us.Filename)
|
|
}
|
|
if err := us.IsValid(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if us.Type == model.UploadTypeAttachment {
|
|
channel, err := a.GetChannel(us.ChannelId)
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.incorrect_channel_id.app_error",
|
|
map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusBadRequest)
|
|
}
|
|
if channel.DeleteAt != 0 {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.cannot_upload_to_deleted_channel.app_error",
|
|
map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
us, storeErr := a.Srv().Store.UploadSession().Save(us)
|
|
if storeErr != nil {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.save.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return us, nil
|
|
}
|
|
|
|
func (a *App) GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError) {
|
|
us, err := a.Srv().Store.UploadSession().Get(uploadId)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
|
|
nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
|
|
nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
return us, nil
|
|
}
|
|
|
|
func (a *App) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) {
|
|
uss, err := a.Srv().Store.UploadSession().GetForUser(userID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUploadsForUser", "app.upload.get_for_user.app_error",
|
|
nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return uss, nil
|
|
}
|
|
|
|
func (a *App) UploadData(c *request.Context, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) {
|
|
// prevent more than one caller to upload data at the same time for a given upload session.
|
|
// This is to avoid possible inconsistencies.
|
|
a.ch.uploadLockMapMut.Lock()
|
|
locked := a.ch.uploadLockMap[us.Id]
|
|
if locked {
|
|
// session lock is already taken, return error.
|
|
a.ch.uploadLockMapMut.Unlock()
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
|
|
nil, "", http.StatusBadRequest)
|
|
}
|
|
// grab the session lock.
|
|
a.ch.uploadLockMap[us.Id] = true
|
|
a.ch.uploadLockMapMut.Unlock()
|
|
|
|
// reset the session lock on exit.
|
|
defer func() {
|
|
a.ch.uploadLockMapMut.Lock()
|
|
delete(a.ch.uploadLockMap, us.Id)
|
|
a.ch.uploadLockMapMut.Unlock()
|
|
}()
|
|
|
|
// fetch the session from store to check for inconsistencies.
|
|
if storedSession, err := a.GetUploadSession(us.Id); err != nil {
|
|
return nil, err
|
|
} else if us.FileOffset != storedSession.FileOffset {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
|
|
nil, "FileOffset mismatch", http.StatusBadRequest)
|
|
}
|
|
|
|
uploadPath := us.Path
|
|
if us.Type == model.UploadTypeImport {
|
|
uploadPath += model.IncompleteUploadSuffix
|
|
}
|
|
|
|
// make sure it's not possible to upload more data than what is expected.
|
|
lr := &io.LimitedReader{
|
|
R: rd,
|
|
N: us.FileSize - us.FileOffset,
|
|
}
|
|
var err *model.AppError
|
|
var written int64
|
|
if us.FileOffset == 0 {
|
|
// new upload
|
|
written, err = a.WriteFile(lr, uploadPath)
|
|
if err != nil && written == 0 {
|
|
return nil, err
|
|
}
|
|
if written < minFirstPartSize && written != us.FileSize {
|
|
a.RemoveFile(uploadPath)
|
|
var errStr string
|
|
if err != nil {
|
|
errStr = err.Error()
|
|
}
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.first_part_too_small.app_error",
|
|
map[string]interface{}{"Size": minFirstPartSize}, errStr, http.StatusBadRequest)
|
|
}
|
|
} else if us.FileOffset < us.FileSize {
|
|
// resume upload
|
|
written, err = a.AppendFile(lr, uploadPath)
|
|
}
|
|
if written > 0 {
|
|
us.FileOffset += written
|
|
if storeErr := a.Srv().Store.UploadSession().Update(us); storeErr != nil {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.update.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// upload is incomplete
|
|
if us.FileOffset != us.FileSize {
|
|
return nil, nil
|
|
}
|
|
|
|
// upload is done, create FileInfo
|
|
file, err := a.FileReader(uploadPath)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.read_file.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
info, err := model.GetInfoForBytes(us.Filename, file, int(us.FileSize))
|
|
file.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info.CreatorId = us.UserId
|
|
info.Path = us.Path
|
|
info.RemoteId = model.NewString(us.RemoteId)
|
|
if us.ReqFileId != "" {
|
|
info.Id = us.ReqFileId
|
|
}
|
|
|
|
// run plugins upload hook
|
|
if err := a.runPluginsHook(c, info, file); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// image post-processing
|
|
if info.IsImage() && !info.IsSvg() {
|
|
if limitErr := checkImageResolutionLimit(info.Width, info.Height, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
|
|
return nil, model.NewAppError("uploadData", "app.upload.upload_data.large_image.app_error",
|
|
map[string]interface{}{"Filename": us.Filename, "Width": info.Width, "Height": info.Height}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")]
|
|
info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview.jpg"
|
|
info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb.jpg"
|
|
imgData, fileErr := a.ReadFile(uploadPath)
|
|
if fileErr != nil {
|
|
return nil, fileErr
|
|
}
|
|
a.HandleImages([]string{info.PreviewPath}, []string{info.ThumbnailPath}, [][]byte{imgData})
|
|
}
|
|
|
|
if us.Type == model.UploadTypeImport {
|
|
if err := a.MoveFile(uploadPath, us.Path); err != nil {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.move_file.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
var storeErr error
|
|
if info, storeErr = a.Srv().Store.FileInfo().Save(info); storeErr != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(storeErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("uploadData", "app.upload.upload_data.save.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if *a.Config().FileSettings.ExtractContent {
|
|
infoCopy := *info
|
|
a.Srv().Go(func() {
|
|
err := a.ExtractContentFromFileInfo(&infoCopy)
|
|
if err != nil {
|
|
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
|
|
}
|
|
})
|
|
}
|
|
|
|
// delete upload session
|
|
if storeErr := a.Srv().Store.UploadSession().Delete(us.Id); storeErr != nil {
|
|
mlog.Warn("Failed to delete UploadSession", mlog.Err(storeErr))
|
|
}
|
|
|
|
return info, nil
|
|
}
|