[MM-63436] Replace Exif parser dependency (#30479)

* Replace Exif parser dependency

* Improve forward seeking logic

* Fix linting

* Stop decoding upon finding tag

* Use latest version of imagemeta dependency

* Don't skip TIFF reader tests

* Log improvements

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Claudio Costa
2025-04-01 13:57:43 -06:00
коммит произвёл GitHub
родитель 2454de5b4a
Коммит f8e16780ef
49 изменённых файлов: 399 добавлений и 74 удалений

Просмотреть файл

@@ -60,7 +60,8 @@ func fileBytes(t *testing.T, path string) []byte {
}
func testDoUploadFileRequest(t testing.TB, c *model.Client4, url string, blob []byte, contentType string,
contentLength int64) (*model.FileUploadResponse, *model.Response, error) {
contentLength int64,
) (*model.FileUploadResponse, *model.Response, error) {
req, err := http.NewRequest("POST", c.APIURL+"/files"+url, bytes.NewReader(blob))
require.NoError(t, err)
@@ -390,6 +391,19 @@ func TestUploadFiles(t *testing.T) {
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
// 5MB+ JPEG
{
title: "Happy image thumbnail/preview 5MB+",
names: []string{"orientation_test_9.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_9_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_9_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{4000},
expectedImageHeights: []int{2667},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
// TIFF preview test
{
title: "Happy image thumbnail/preview 9",

Просмотреть файл

@@ -1664,7 +1664,6 @@ func getTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
@@ -1742,7 +1741,7 @@ func setTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
imageData := imageArray[0]
if err := c.App.SetTeamIcon(c.Params.TeamId, imageData); err != nil {
if err := c.App.SetTeamIcon(c.AppContext, c.Params.TeamId, imageData); err != nil {
c.Err = err
return
}

Просмотреть файл

@@ -770,11 +770,8 @@ func (t *UploadFileTask) init(a *App) {
// upload, returning a rejection error. In this case FileInfo would have
// contained the last "good" FileInfo before the execution of that plugin.
func (a *App) UploadFileX(c request.CTX, channelID, name string, input io.Reader,
opts ...func(*UploadFileTask)) (*model.FileInfo, *model.AppError) {
c = c.WithLogger(c.Logger().With(
mlog.String("file_name", name),
))
opts ...func(*UploadFileTask),
) (*model.FileInfo, *model.AppError) {
t := &UploadFileTask{
Logger: c.Logger(),
ChannelId: filepath.Base(channelID),
@@ -790,6 +787,12 @@ func (a *App) UploadFileX(c request.CTX, channelID, name string, input io.Reader
o(t)
}
c = c.WithLogger(c.Logger().With(
mlog.String("file_name", name),
mlog.String("channel_id", channelID),
mlog.String("user_id", t.UserId),
))
if *a.Config().FileSettings.DriverName == "" {
return nil, t.newAppError("api.file.upload_file.storage.app_error", http.StatusNotImplemented)
}
@@ -880,14 +883,14 @@ func (t *UploadFileTask) preprocessImage() *model.AppError {
}
// If we fail to decode, return "as is".
w, h, err := imaging.GetDimensions(t.teeInput)
cfg, format, err := t.imgDecoder.DecodeConfig(t.teeInput)
if err != nil {
return nil
}
t.fileinfo.Width = w
t.fileinfo.Height = h
t.fileinfo.Width = cfg.Width
t.fileinfo.Height = cfg.Height
if err = checkImageResolutionLimit(w, h, t.maxImageRes); err != nil {
if err = checkImageResolutionLimit(cfg.Width, cfg.Height, t.maxImageRes); err != nil {
return t.newAppError("api.file.upload_file.large_image_detailed.app_error", http.StatusBadRequest)
}
@@ -899,12 +902,14 @@ func (t *UploadFileTask) preprocessImage() *model.AppError {
// check the image orientation with goexif; consume the bytes we
// already have first, then keep Tee-ing from input.
// TODO: try to reuse exif's .Raw buffer rather than Tee-ing
if t.imageOrientation, err = imaging.GetImageOrientation(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput)); err == nil &&
if t.imageOrientation, err = imaging.GetImageOrientation(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput), format); err == nil &&
(t.imageOrientation == imaging.RotatedCWMirrored ||
t.imageOrientation == imaging.RotatedCCW ||
t.imageOrientation == imaging.RotatedCCWMirrored ||
t.imageOrientation == imaging.RotatedCW) {
t.fileinfo.Width, t.fileinfo.Height = t.fileinfo.Height, t.fileinfo.Width
} else if err != nil {
t.Logger.Warn("Failed to get image orientation", mlog.Err(err))
}
// For animated GIFs disable the preview; since we have to Decode gifs
@@ -1045,12 +1050,14 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe
return nil, data, err
}
if orientation, err := imaging.GetImageOrientation(bytes.NewReader(data)); err == nil &&
if orientation, err := imaging.GetImageOrientation(bytes.NewReader(data), info.MimeType); err == nil &&
(orientation == imaging.RotatedCWMirrored ||
orientation == imaging.RotatedCCW ||
orientation == imaging.RotatedCCWMirrored ||
orientation == imaging.RotatedCW) {
info.Width, info.Height = info.Height, info.Width
} else if err != nil {
c.Logger().Warn("Failed to get image orientation", mlog.Err(err))
}
info.Id = model.NewId()
@@ -1161,7 +1168,7 @@ func prepareImage(rctx request.CTX, imgDecoder *imaging.Decoder, imgData io.Read
imgData.Seek(0, io.SeekStart)
// Flip the image to be upright
orientation, err := imaging.GetImageOrientation(imgData)
orientation, err := imaging.GetImageOrientation(imgData, imgType)
if err != nil {
rctx.Logger().Debug("GetImageOrientation failed", mlog.Err(err))
}

Просмотреть файл

@@ -4,12 +4,14 @@
package imaging
import (
"errors"
"fmt"
"image"
"io"
"strings"
"github.com/anthonynsimon/bild/transform"
"github.com/rwcarlsen/goexif/exif"
"github.com/bep/imagemeta"
)
const (
@@ -33,6 +35,8 @@ const (
RotatedCW
)
var errStopDecoding = fmt.Errorf("stop decoding")
// MakeImageUpright changes the orientation of the given image.
func MakeImageUpright(img image.Image, orientation int) image.Image {
switch orientation {
@@ -55,23 +59,101 @@ func MakeImageUpright(img image.Image, orientation int) image.Image {
}
}
type fwSeeker struct {
r io.Reader
pos int64
}
func (f *fwSeeker) Read(p []byte) (int, error) {
n, err := f.r.Read(p)
if err != nil {
return n, err
}
f.pos += int64(n)
return n, nil
}
func (f *fwSeeker) Seek(offset int64, whence int) (int64, error) {
isForwardSeek := (whence == io.SeekStart && offset >= f.pos) ||
(whence == io.SeekCurrent && offset >= 0)
// We only support seeking forward.
if !isForwardSeek {
return 0, fmt.Errorf("seeking backwards is not supported")
}
toRead := offset
if whence == io.SeekStart {
toRead -= f.pos
}
// Seeking forward means we can simply discard the data.
n, err := io.CopyN(io.Discard, f.r, toRead)
if err != nil {
return n, fmt.Errorf("failed to seek: %w", err)
}
f.pos += n
return f.pos, nil
}
// GetImageOrientation reads the input data and returns the EXIF encoded
// image orientation.
func GetImageOrientation(input io.Reader) (int, error) {
exifData, err := exif.Decode(input)
if err != nil {
// image orientation. Supported formats are JPEG, PNG, TIFF, and WebP.
// Passing an io.ReadSeeker is preferable as we can't guarantee a plain
// io.Reader will work for all formats (e.g. TIFF requires backwards seeking).
func GetImageOrientation(input io.Reader, format string) (int, error) {
orientation := Upright
// Strip the "image/" prefix from the format in case it's a MIME type.
format, _ = strings.CutPrefix(format, "image/")
var imgFormat imagemeta.ImageFormat
switch format {
case "jpeg":
imgFormat = imagemeta.JPEG
case "png":
imgFormat = imagemeta.PNG
case "tiff":
imgFormat = imagemeta.TIFF
case "webp":
imgFormat = imagemeta.WebP
default:
// We don't support EXIF on any other format.
return orientation, fmt.Errorf("unsupported image format: %s", format)
}
var rs io.ReadSeeker
if r, ok := input.(io.ReadSeeker); ok {
rs = r
} else {
rs = &fwSeeker{r: input}
}
opts := imagemeta.Options{
R: rs,
HandleTag: func(tag imagemeta.TagInfo) error {
if tag.Tag == "Orientation" {
if o, ok := tag.Value.(uint16); ok {
orientation = int(o)
// Stop decoding after we've found the orientation tag]
// since it's the only one we care about.
return errStopDecoding
}
}
return nil
},
ShouldHandleTag: func(tag imagemeta.TagInfo) bool {
// We only care about the orientation tag.
return tag.Tag == "Orientation"
},
Sources: imagemeta.EXIF, // We only care about EXIF data.
ImageFormat: imgFormat,
}
if err := imagemeta.Decode(opts); err != nil && !errors.Is(err, errStopDecoding) {
return Upright, fmt.Errorf("failed to decode exif data: %w", err)
}
tag, err := exifData.Get("Orientation")
if err != nil {
return Upright, fmt.Errorf("failed to get orientation field from exif data: %w", err)
}
orientation, err := tag.Int(0)
if err != nil {
return Upright, fmt.Errorf("failed to get value from exif tag: %w", err)
}
return orientation, nil
}

Просмотреть файл

@@ -0,0 +1,199 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
"github.com/stretchr/testify/require"
)
func TestFwSeeker(t *testing.T) {
t.Run("Read", func(t *testing.T) {
data := []byte("hello world")
reader := bytes.NewReader(data)
seeker := &fwSeeker{r: reader}
// Read the first 5 bytes
buf := make([]byte, 5)
n, err := seeker.Read(buf)
require.NoError(t, err)
require.Equal(t, 5, n)
require.Equal(t, []byte("hello"), buf)
// Read the next 6 bytes
buf = make([]byte, 6)
n, err = seeker.Read(buf)
require.NoError(t, err)
require.Equal(t, 6, n)
require.Equal(t, []byte(" world"), buf)
// Try to read more, should get EOF
buf = make([]byte, 1)
n, err = seeker.Read(buf)
require.Equal(t, 0, n)
require.Equal(t, io.EOF, err)
})
t.Run("Seek forward from start", func(t *testing.T) {
data := []byte("hello world")
reader := bytes.NewReader(data)
seeker := &fwSeeker{r: reader}
// Seek forward 6 bytes
pos, err := seeker.Seek(6, io.SeekStart)
require.NoError(t, err)
require.Equal(t, int64(6), pos)
// Seeking from the same position should work
pos, err = seeker.Seek(6, io.SeekStart)
require.NoError(t, err)
require.Equal(t, int64(6), pos)
// Seeking again from start should work.
pos, err = seeker.Seek(7, io.SeekStart)
require.NoError(t, err)
require.Equal(t, int64(7), pos)
// Seeking backwards should not be supported
_, err = seeker.Seek(6, io.SeekStart)
require.EqualError(t, err, "seeking backwards is not supported")
// Read the remaining data
buf := make([]byte, 4)
n, err := seeker.Read(buf)
require.NoError(t, err)
require.Equal(t, 4, n)
require.Equal(t, []byte("orld"), buf)
})
t.Run("Seek forward from current", func(t *testing.T) {
data := []byte("hello world")
reader := bytes.NewReader(data)
seeker := &fwSeeker{r: reader}
// Read first 6 bytes
buf := make([]byte, 6)
n, err := seeker.Read(buf)
require.NoError(t, err)
require.Equal(t, 6, n)
require.Equal(t, []byte("hello "), buf)
// Seek forward 2 more bytes from current position
pos, err := seeker.Seek(2, io.SeekCurrent)
require.NoError(t, err)
require.Equal(t, int64(8), pos)
// Read the remaining data
buf = make([]byte, 3)
n, err = seeker.Read(buf)
require.NoError(t, err)
require.Equal(t, 3, n)
require.Equal(t, []byte("rld"), buf)
})
t.Run("Seek backward not supported", func(t *testing.T) {
data := []byte("hello world")
reader := bytes.NewReader(data)
seeker := &fwSeeker{r: reader}
// Try to seek backward
_, err := seeker.Seek(-1, io.SeekCurrent)
require.EqualError(t, err, "seeking backwards is not supported")
// Try to seek from end
_, err = seeker.Seek(0, io.SeekEnd)
require.EqualError(t, err, "seeking backwards is not supported")
})
t.Run("Seek beyond EOF", func(t *testing.T) {
data := []byte("hello")
reader := bytes.NewReader(data)
seeker := &fwSeeker{r: reader}
// Try to seek beyond EOF
n, err := seeker.Seek(10, io.SeekStart)
require.EqualError(t, err, "failed to seek: EOF")
require.Equal(t, int64(5), n) // Should have read until EOF (5 bytes)
})
}
func TestGetImageOrientation(t *testing.T) {
imgDir, ok := fileutils.FindDir("tests/exif_samples")
require.True(t, ok, "Failed to find exif samples directory")
// Define orientations and their corresponding file prefixes
orientations := map[string]int{
"up": Upright,
"up-mirrored": UprightMirrored,
"down": UpsideDown,
"down-mirrored": UpsideDownMirrored,
"left": RotatedCCW,
"left-mirrored": RotatedCWMirrored,
"right": RotatedCW,
"right-mirrored": RotatedCCWMirrored,
}
// Define supported formats
formats := []string{"jpg", "png", "tiff", "webp"}
// Generate test cases for all combinations
var testCases []struct {
name string
fileName string
expectedOrientation int
}
for prefix, orientation := range orientations {
for _, format := range formats {
testCases = append(testCases, struct {
name string
fileName string
expectedOrientation int
}{
name: fmt.Sprintf("%s (%s)", prefix, format),
fileName: fmt.Sprintf("%s.%s", prefix, format),
expectedOrientation: orientation,
})
}
}
dec, err := NewDecoder(DecoderOptions{})
require.NoError(t, err)
for _, tc := range testCases {
var orientation int
imgPath := filepath.Join(imgDir, tc.fileName)
file, err := os.Open(imgPath)
require.NoError(t, err)
defer file.Close()
_, format, err := dec.DecodeConfig(file)
require.NoError(t, err)
t.Run(tc.name+"_file", func(t *testing.T) {
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)
orientation, err = GetImageOrientation(file, format)
require.NoError(t, err)
require.Equal(t, tc.expectedOrientation, orientation, "Incorrect orientation detected for %s", tc.fileName)
})
t.Run(tc.name+"_reader", func(t *testing.T) {
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)
orientation, err = GetImageOrientation(&io.LimitedReader{R: file, N: 1024 * 1024}, format)
require.NoError(t, err)
require.Equal(t, tc.expectedOrientation, orientation, "Incorrect orientation detected for %s", tc.fileName)
})
}
}

Просмотреть файл

@@ -948,7 +948,7 @@ func (api *PluginAPI) SetTeamIcon(teamID string, data []byte) *model.AppError {
return err
}
return api.app.SetTeamIconFromFile(team, bytes.NewReader(data))
return api.app.SetTeamIconFromFile(api.ctx, team, bytes.NewReader(data))
}
func (api *PluginAPI) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
@@ -1103,12 +1103,15 @@ func (api *PluginAPI) UpdateUserRoles(userID string, newRoles string) (*model.Us
func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...any) {
api.logger.Debugw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogInfo(msg string, keyValuePairs ...any) {
api.logger.Infow(msg, keyValuePairs...)
}
func (api *PluginAPI) LogError(msg string, keyValuePairs ...any) {
api.logger.Errorw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...any) {
api.logger.Warnw(msg, keyValuePairs...)
}
@@ -1320,7 +1323,8 @@ func (api *PluginAPI) DeleteOAuthApp(appID string) *model.AppError {
// PublishPluginClusterEvent broadcasts a plugin event to all other running instances of
// the calling plugin.
func (api *PluginAPI) PublishPluginClusterEvent(ev model.PluginClusterEvent,
opts model.PluginClusterEventSendOptions) error {
opts model.PluginClusterEventSendOptions,
) error {
if api.app.Cluster() == nil {
return nil
}

Просмотреть файл

@@ -1177,7 +1177,7 @@ func TestPluginAPIGetTeamIcon(t *testing.T) {
fileReader := bytes.NewReader(dataBytes)
// Set the Team Icon
appErr := th.App.SetTeamIconFromFile(th.BasicTeam, fileReader)
appErr := th.App.SetTeamIconFromFile(th.Context, th.BasicTeam, fileReader)
require.Nil(t, appErr)
// Get the team icon to check
@@ -1239,7 +1239,7 @@ func TestPluginAPIRemoveTeamIcon(t *testing.T) {
fileReader := bytes.NewReader(dataBytes)
// Set the Team Icon
err := th.App.SetTeamIconFromFile(th.BasicTeam, fileReader)
err := th.App.SetTeamIconFromFile(th.Context, th.BasicTeam, fileReader)
require.Nil(t, err)
err = api.RemoveTeamIcon(th.BasicTeam.Id)
require.Nil(t, err)

Просмотреть файл

@@ -650,7 +650,6 @@ func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64,
var err error
if looksLikeAPermalink(requestURL, a.GetSiteURL()) && *a.Config().ServiceSettings.EnablePermalinkPreviews {
permalink, err = a.getLinkMetadataForPermalink(c, requestURL)
if err != nil {
return nil, nil, nil, err
}
@@ -784,7 +783,7 @@ func (a *App) getLinkMetadataForURL(c request.CTX, requestURL string) (*opengrap
if err == nil {
// Parse the data
og, image, err = a.parseLinkMetadata(requestURL, body, contentType)
og, image, err = a.parseLinkMetadata(c, requestURL, body, contentType)
}
og = model.TruncateOpenGraph(og) // remove unwanted length of texts
@@ -878,7 +877,7 @@ func peekContentType(p *bufio.Reader) string {
return http.DetectContentType(byt)
}
func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType string) (*opengraph.OpenGraph, *model.PostImage, error) {
func (a *App) parseLinkMetadata(rctx request.CTX, requestURL string, body io.Reader, contentType string) (*opengraph.OpenGraph, *model.PostImage, error) {
if contentType == "" {
bufRd := bufio.NewReader(body)
// If the content-type is missing we try to detect it from the actual data.
@@ -893,7 +892,7 @@ func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType s
return nil, image, nil
} else if strings.HasPrefix(contentType, "image") {
image, err := parseImages(io.LimitReader(body, MaxMetadataImageSize))
image, err := parseImages(rctx, requestURL, io.LimitReader(body, MaxMetadataImageSize))
return nil, image, err
} else if strings.HasPrefix(contentType, "text/html") {
og := a.parseOpenGraphMetadata(requestURL, body, contentType)
@@ -909,7 +908,7 @@ func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType s
return nil, nil, nil
}
func parseImages(body io.Reader) (*model.PostImage, error) {
func parseImages(rctx request.CTX, requestURL string, body io.Reader) (*model.PostImage, error) {
// Store any data that is read for the config for any further processing
buf := &bytes.Buffer{}
t := io.TeeReader(body, buf)
@@ -927,12 +926,14 @@ func parseImages(body io.Reader) (*model.PostImage, error) {
}
if format == "jpeg" {
if imageOrientation, err := imaging.GetImageOrientation(io.MultiReader(buf, body)); err == nil &&
if imageOrientation, err := imaging.GetImageOrientation(io.MultiReader(buf, body), format); err == nil &&
(imageOrientation == imaging.RotatedCWMirrored ||
imageOrientation == imaging.RotatedCCW ||
imageOrientation == imaging.RotatedCCWMirrored ||
imageOrientation == imaging.RotatedCW) {
image.Width, image.Height = image.Height, image.Width
} else if err != nil {
rctx.Logger().Warn("Failed to get image orientation", mlog.Err(err), mlog.String("request_url", requestURL))
}
}

Просмотреть файл

@@ -29,6 +29,8 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/httpservice"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
@@ -2673,7 +2675,7 @@ func TestParseLinkMetadata(t *testing.T) {
}
t.Run("image", func(t *testing.T) {
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeImageReader(), "image/png")
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeImageReader(), "image/png")
assert.NoError(t, err)
assert.Nil(t, og)
@@ -2685,7 +2687,7 @@ func TestParseLinkMetadata(t *testing.T) {
})
t.Run("image with no content-type given", func(t *testing.T) {
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeImageReader(), "")
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeImageReader(), "")
assert.NoError(t, err)
assert.Nil(t, og)
@@ -2697,7 +2699,7 @@ func TestParseLinkMetadata(t *testing.T) {
})
t.Run("malformed image", func(t *testing.T) {
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeOpenGraphReader(), "image/png")
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeOpenGraphReader(), "image/png")
assert.Error(t, err)
assert.Nil(t, og)
@@ -2705,7 +2707,7 @@ func TestParseLinkMetadata(t *testing.T) {
})
t.Run("opengraph", func(t *testing.T) {
og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeOpenGraphReader(), "text/html; charset=utf-8")
og, dimensions, err := th.App.parseLinkMetadata(th.Context, ogURL, makeOpenGraphReader(), "text/html; charset=utf-8")
assert.NoError(t, err)
assert.NotNil(t, og)
@@ -2716,7 +2718,7 @@ func TestParseLinkMetadata(t *testing.T) {
})
t.Run("malformed opengraph", func(t *testing.T) {
og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeImageReader(), "text/html; charset=utf-8")
og, dimensions, err := th.App.parseLinkMetadata(th.Context, ogURL, makeImageReader(), "text/html; charset=utf-8")
assert.NoError(t, err)
assert.Nil(t, og)
@@ -2724,7 +2726,7 @@ func TestParseLinkMetadata(t *testing.T) {
})
t.Run("neither", func(t *testing.T) {
og, dimensions, err := th.App.parseLinkMetadata("http://example.com/test.wad", strings.NewReader("garbage"), "application/x-doom")
og, dimensions, err := th.App.parseLinkMetadata(th.Context, "http://example.com/test.wad", strings.NewReader("garbage"), "application/x-doom")
assert.NoError(t, err)
assert.Nil(t, og)
@@ -2732,7 +2734,7 @@ func TestParseLinkMetadata(t *testing.T) {
})
t.Run("svg", func(t *testing.T) {
og, dimensions, err := th.App.parseLinkMetadata("http://example.com/image.svg", nil, "image/svg+xml")
og, dimensions, err := th.App.parseLinkMetadata(th.Context, "http://example.com/image.svg", nil, "image/svg+xml")
assert.NoError(t, err)
assert.Nil(t, og)
@@ -2820,6 +2822,14 @@ func TestParseImages(t *testing.T) {
Format: "jpeg",
},
},
"jpg-9": {
FileName: "orientation_test_9.jpeg",
Expected: &model.PostImage{
Width: 4000,
Height: 2667,
Format: "jpeg",
},
},
"animated gif": {
FileName: "testgif.gif",
Expected: &model.PostImage{
@@ -2842,7 +2852,7 @@ func TestParseImages(t *testing.T) {
file, err := testutils.ReadTestFile(testCase.FileName)
require.NoError(t, err)
result, err := parseImages(bytes.NewReader(file))
result, err := parseImages(request.EmptyContext(mlog.CreateConsoleTestLogger(t)), "", bytes.NewReader(file))
if testCase.ExpectError {
assert.Error(t, err)
} else {

Просмотреть файл

@@ -1944,16 +1944,16 @@ func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
return data, nil
}
func (a *App) SetTeamIcon(teamID string, imageData *multipart.FileHeader) *model.AppError {
func (a *App) SetTeamIcon(rctx request.CTX, teamID string, imageData *multipart.FileHeader) *model.AppError {
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
return a.SetTeamIconFromMultiPartFile(teamID, file)
return a.SetTeamIconFromMultiPartFile(rctx, teamID, file)
}
func (a *App) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError {
func (a *App) SetTeamIconFromMultiPartFile(rctx request.CTX, teamID string, file multipart.File) *model.AppError {
team, getTeamErr := a.GetTeam(teamID)
if getTeamErr != nil {
@@ -1969,17 +1969,21 @@ func (a *App) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *
nil, "", http.StatusBadRequest).Wrap(limitErr)
}
return a.SetTeamIconFromFile(team, file)
return a.SetTeamIconFromFile(rctx, team, file)
}
func (a *App) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError {
func (a *App) SetTeamIconFromFile(rctx request.CTX, team *model.Team, file io.ReadSeeker) *model.AppError {
// Decode image into Image object
img, _, err := image.Decode(file)
img, format, err := image.Decode(file)
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
orientation, _ := imaging.GetImageOrientation(file)
orientation, err := imaging.GetImageOrientation(file, format)
if err != nil {
rctx.Logger().Warn("Failed to get image orientation", mlog.Err(err))
}
img = imaging.MakeImageUpright(img, orientation)
// Scale team icon

Просмотреть файл

@@ -882,14 +882,18 @@ func (a *App) SetProfileImageFromMultiPartFile(c request.CTX, userID string, fil
return a.SetProfileImageFromFile(c, userID, file)
}
func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
func (a *App) AdjustImage(rctx request.CTX, file io.ReadSeeker) (*bytes.Buffer, *model.AppError) {
// Decode image into Image object
img, _, err := a.ch.imgDecoder.Decode(file)
img, format, err := a.ch.imgDecoder.Decode(file)
if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
orientation, _ := imaging.GetImageOrientation(file)
orientation, err := imaging.GetImageOrientation(file, format)
if err != nil {
rctx.Logger().Warn("Failed to get image orientation", mlog.Err(err))
}
img = imaging.MakeImageUpright(img, orientation)
// Scale profile image
@@ -904,8 +908,8 @@ func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
return buf, nil
}
func (a *App) SetProfileImageFromFile(c request.CTX, userID string, file io.Reader) *model.AppError {
buf, err := a.AdjustImage(file)
func (a *App) SetProfileImageFromFile(c request.CTX, userID string, file io.ReadSeeker) *model.AppError {
buf, err := a.AdjustImage(c, file)
if err != nil {
return err
}
@@ -1363,7 +1367,6 @@ func (a *App) UpdateUser(c request.CTX, user *model.User, sendNotifications bool
func (a *App) UpdateUserActive(c request.CTX, userID string, active bool) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
@@ -1671,7 +1674,6 @@ func (a *App) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppEr
func (a *App) GetTokenById(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
var status int
@@ -2043,7 +2045,6 @@ func (a *App) VerifyUserEmail(userID, email string) *model.AppError {
a.InvalidateCacheForUser(userID)
user, err := a.GetUser(userID)
if err != nil {
return err
}
@@ -2788,7 +2789,6 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, tea
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.IsPostPriorityEnabled())
if err != nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {

Просмотреть файл

@@ -112,14 +112,14 @@ func TestAdjustProfileImage(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
_, appErr := th.App.AdjustImage(bytes.NewReader([]byte{}))
_, appErr := th.App.AdjustImage(th.Context, bytes.NewReader([]byte{}))
require.NotNil(t, appErr)
// test image isn't the correct dimensions
// it should be adjusted
testjpg, err := testutils.ReadTestFile("testjpg.jpg")
require.NoError(t, err)
adjusted, appErr := th.App.AdjustImage(bytes.NewReader(testjpg))
adjusted, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(testjpg))
require.Nil(t, appErr)
assert.True(t, adjusted.Len() > 0)
assert.NotEqual(t, testjpg, adjusted)
@@ -128,7 +128,7 @@ func TestAdjustProfileImage(t *testing.T) {
user := th.BasicUser
image, appErr := th.App.GetDefaultProfileImage(user)
require.Nil(t, appErr)
image2, appErr := th.App.AdjustImage(bytes.NewReader(image))
image2, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(image))
require.Nil(t, appErr)
assert.Equal(t, image, image2.Bytes())
}
@@ -317,7 +317,8 @@ func TestCreateUser(t *testing.T) {
func main() {
plugin.ClientMain(&MyPlugin{})
}
`}, th.App, th.NewPluginAPI)
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
user := &model.User{
@@ -1924,7 +1925,7 @@ func TestUpdateUserRolesWithUser(t *testing.T) {
_, err = th.App.UpdateUserRolesWithUser(th.Context, user, "does not exist", false)
require.NotNil(t, err)
//Test reset to User role
// Test reset to User role
user, err = th.App.UpdateUserRolesWithUser(th.Context, user, model.SystemUserRoleId, false)
require.Nil(t, err)
assert.Equal(t, user.Roles, model.SystemUserRoleId)

Просмотреть файл

@@ -10,6 +10,7 @@ require (
github.com/anthonynsimon/bild v0.14.0
github.com/avct/uasurfer v0.0.0-20240501094946-ca0c4d1e541b
github.com/aws/aws-sdk-go v1.55.6
github.com/bep/imagemeta v0.10.0
github.com/blang/semver/v4 v4.0.0
github.com/blevesearch/bleve/v2 v2.4.4-0.20250115090822-cbafdca08538
github.com/cespare/xxhash/v2 v2.3.0
@@ -59,7 +60,6 @@ require (
github.com/reflog/dateconstraints v0.2.1
github.com/rs/cors v1.11.1
github.com/rudderlabs/analytics-go v3.3.3+incompatible
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
@@ -75,7 +75,7 @@ require (
golang.org/x/crypto v0.32.0
golang.org/x/image v0.23.0
golang.org/x/net v0.34.0
golang.org/x/sync v0.10.0
golang.org/x/sync v0.12.0
golang.org/x/term v0.28.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v3 v3.0.1
@@ -222,7 +222,7 @@ require (
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/grpc v1.70.0 // indirect

Просмотреть файл

@@ -56,6 +56,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bep/imagemeta v0.10.0 h1:kbQxe8SLHTUEEOT/DvLMGslIJy9Z1R+ZAckkIUnUBw4=
github.com/bep/imagemeta v0.10.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
@@ -758,8 +760,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -821,8 +824,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=

Двоичные данные
server/tests/exif_samples/down-mirrored.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичные данные
server/tests/exif_samples/down-mirrored.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичные данные
server/tests/exif_samples/down-mirrored.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/down-mirrored.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.5 KiB

Двоичные данные
server/tests/exif_samples/down.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.8 KiB

Двоичные данные
server/tests/exif_samples/down.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.6 KiB

Двоичные данные
server/tests/exif_samples/down.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/down.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.3 KiB

Двоичные данные
server/tests/exif_samples/left-mirrored.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 13 KiB

Двоичные данные
server/tests/exif_samples/left-mirrored.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичные данные
server/tests/exif_samples/left-mirrored.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/left-mirrored.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Двоичные данные
server/tests/exif_samples/left.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.8 KiB

Двоичные данные
server/tests/exif_samples/left.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.6 KiB

Двоичные данные
server/tests/exif_samples/left.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/left.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичные данные
server/tests/exif_samples/right-mirrored.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичные данные
server/tests/exif_samples/right-mirrored.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичные данные
server/tests/exif_samples/right-mirrored.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/right-mirrored.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.8 KiB

Двоичные данные
server/tests/exif_samples/right.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.8 KiB

Двоичные данные
server/tests/exif_samples/right.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.1 KiB

Двоичные данные
server/tests/exif_samples/right.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/right.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Двоичные данные
server/tests/exif_samples/up-mirrored.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичные данные
server/tests/exif_samples/up-mirrored.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 11 KiB

Двоичные данные
server/tests/exif_samples/up-mirrored.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/up-mirrored.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.4 KiB

Двоичные данные
server/tests/exif_samples/up.jpg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.2 KiB

Двоичные данные
server/tests/exif_samples/up.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.9 KiB

Двоичные данные
server/tests/exif_samples/up.tiff Обычный файл

Двоичный файл не отображается.

Двоичные данные
server/tests/exif_samples/up.webp Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичные данные
server/tests/orientation_test_9.jpeg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.8 MiB

Двоичные данные
server/tests/orientation_test_9_expected_preview.jpeg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 688 KiB

Двоичные данные
server/tests/orientation_test_9_expected_thumb.jpeg Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.1 KiB