Files
team/server/channels/app/users/profile_picture.go
Alejandro García MontoroandGitHub 82106eb77e MM-69213: Validate Azure storage account names and use httpservice for custom endpoints (#37014)
* Validate Azure storage account name format

For the commercial and government clouds the storage account name
becomes part of the service hostname, so enforce Azure's documented
format (3 to 24 lowercase letters and digits) both in config
validation and when building the service URL. Also treat an empty
AzureCloud as commercial, consistent with the URL builder.

* Route Azure custom endpoints through httpservice

The Azure custom-cloud backend now issues requests through
httpservice's transport rather than the SDK default, so they honor
ServiceSettings.AllowedUntrustedInternalConnections like other
outbound connections. The commercial and government clouds keep the
SDK default transport, which Private Link deployments rely on.

The allowlist value is plumbed into FileBackendSettings, and the
host/IP checks are factored into a shared httpservice helper that
MakeTransport now uses too.
2026-06-15 15:46:42 +02:00

205 lines
4.8 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"bytes"
"hash/fnv"
"image"
"image/color"
"image/draw"
"image/png"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
xfont "golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
const (
imageProfilePixelDimension = 128
)
func (us *UserService) GetProfileImage(user *model.User) ([]byte, bool, error) {
if *us.config().FileSettings.DriverName == "" {
img, err := us.GetDefaultProfileImage(user)
if err != nil {
return nil, false, err
}
return img, false, nil
}
path := path.Join("users", user.Id, "profile.png")
data, err := us.ReadFile(path)
if err != nil {
img, appErr := us.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
if user.LastPictureUpdate == 0 {
if _, err := us.writeFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
return img, true, nil
}
return data, false, nil
}
func (us *UserService) FileBackend() (filestore.FileBackend, error) {
license := us.license()
insecure := us.config().ServiceSettings.EnableInsecureOutgoingConnections
allowedUntrustedInternalConnections := model.SafeDereference(us.config().ServiceSettings.AllowedUntrustedInternalConnections)
backend, err := filestore.NewFileBackend(filestore.NewFileBackendSettingsFromConfig(&us.config().FileSettings, license != nil && *license.Features.Compliance, insecure != nil && *insecure, allowedUntrustedInternalConnections))
if err != nil {
return nil, err
}
return backend, nil
}
func (us *UserService) ReadFile(path string) ([]byte, error) {
backend, err := us.FileBackend()
if err != nil {
return nil, err
}
result, nErr := backend.ReadFile(path)
if nErr != nil {
return nil, nErr
}
return result, nil
}
func (us *UserService) writeFile(fr io.Reader, path string) (int64, error) {
backend, err := us.FileBackend()
if err != nil {
return 0, err
}
result, nErr := backend.WriteFile(fr, path)
if nErr != nil {
return result, nErr
}
return result, nil
}
func (us *UserService) GetDefaultProfileImage(user *model.User) ([]byte, error) {
if user.IsBot {
return botDefaultImage, nil
}
return createProfileImage(user.Username, user.Id, *us.config().FileSettings.InitialFont)
}
func createProfileImage(username string, userID string, initialFont string) ([]byte, error) {
colors := []color.NRGBA{
{197, 8, 126, 255},
{227, 207, 18, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
}
h := fnv.New32a()
h.Write([]byte(userID))
seed := h.Sum32()
initial := string(strings.ToUpper(username)[0])
font, err := getFont(initialFont)
if err != nil {
return nil, DefaultFontError
}
color := colors[int64(seed)%int64(len(colors))]
dstImg := image.NewRGBA(image.Rect(0, 0, imageProfilePixelDimension, imageProfilePixelDimension))
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.Point{}, draw.Src)
size := float64(imageProfilePixelDimension / 2)
opts := truetype.Options{}
opts.Size = size
face := truetype.NewFace(font, &opts)
d := &xfont.Drawer{
Dst: dstImg,
Src: image.White,
Face: face,
}
bounds, advance, ok := face.GlyphBounds([]rune(initial)[0])
if !ok {
return nil, GlyphError
}
x := (fixed.I(imageProfilePixelDimension) - advance) / 2
y := (fixed.I(imageProfilePixelDimension) + (bounds.Max.Y - bounds.Min.Y)) / 2
d.Dot = fixed.Point26_6{
X: x,
Y: y,
}
d.DrawString(initial)
buf := new(bytes.Buffer)
enc := png.Encoder{
CompressionLevel: png.BestCompression,
}
if imgErr := enc.Encode(buf, dstImg); imgErr != nil {
return nil, ImageEncodingError
}
return buf.Bytes(), nil
}
func getFont(initialFont string) (*truetype.Font, error) {
// Some people have the old default font still set, so just treat that as if they're using the new default
if initialFont == "luximbi.ttf" {
initialFont = "nunito-bold.ttf"
}
fontDir, _ := fileutils.FindDir("fonts")
fontBytes, err := os.ReadFile(filepath.Join(fontDir, initialFont))
if err != nil {
return nil, err
}
return freetype.ParseFont(fontBytes)
}