package useCases import ( "RenewCMS/internal/domain/audit" "RenewCMS/internal/domain/image" "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/xml" stdimage "image" "io" "strings" _ "golang.org/x/image/webp" _ "image/jpeg" _ "image/png" ) const svgRootElement = "svg" var rasterFormats = map[string]string{ "image/png": "png", "image/jpeg": "jpeg", "image/webp": "webp", } type CreateImageUseCase struct { imageRepository image.Repository storage image.Storage recorder audit.Recorder } func NewCreateImageUseCase(imageRepository image.Repository, storage image.Storage, recorder audit.Recorder) *CreateImageUseCase { return &CreateImageUseCase{imageRepository, storage, recorder} } func (g *CreateImageUseCase) CreateImage(ctx context.Context, input image.ImageInput) (image.Image, error) { created, err := g.create(input) event := audit.NewEvent(audit.ActionMediaUpload, audit.CategoryMedia) if err != nil { event.Target = attemptedUploadTarget(input) recordFailure(ctx, g.recorder, event, err) return image.Image{}, err } event.Target = imageTarget(created) g.recorder.Record(ctx, event) return created, nil } func imageTarget(img image.Image) audit.Target { return audit.NewTarget(audit.TargetImage, img.ID, img.Filename) } func attemptedUploadTarget(input image.ImageInput) audit.Target { if strings.TrimSpace(input.Filename) == "" { return audit.Target{} } return audit.Target{Type: audit.TargetImage, Name: image.NormalizeUploadFilename(input.Filename, input.ContentType)} } func (g *CreateImageUseCase) create(input image.ImageInput) (image.Image, error) { if !image.IsAllowedContentType(input.ContentType) { return image.Image{}, image.ErrImageTypeNotAllowed } data, err := readWithinLimit(input.Content) if err != nil { return image.Image{}, err } width, height, err := measure(data, input.ContentType) if err != nil { return image.Image{}, err } checksum := sha256.Sum256(data) storedName, err := g.storage.Put(input.ContentType, bytes.NewReader(data)) if err != nil { return image.Image{}, err } created, err := g.imageRepository.Create(image.Image{ Path: image.PublicPath(storedName), Filename: image.NormalizeUploadFilename(input.Filename, input.ContentType), Size: int64(len(data)), ContentType: input.ContentType, Width: width, Height: height, SHA256: hex.EncodeToString(checksum[:]), UploadedByID: input.UploadedByID, }) if err != nil { _ = g.storage.Delete(storedName) return image.Image{}, err } return created, nil } func readWithinLimit(content io.Reader) ([]byte, error) { if content == nil { return nil, image.ErrImageUnreadable } data, err := io.ReadAll(io.LimitReader(content, image.MaxImageSize+1)) if err != nil { return nil, err } if int64(len(data)) > image.MaxImageSize { return nil, image.ErrImageTooLarge } return data, nil } func measure(data []byte, contentType string) (int, int, error) { if contentType == "image/svg+xml" { if !hasSvgRoot(data) { return 0, 0, image.ErrImageUnreadable } return 0, 0, nil } config, format, err := stdimage.DecodeConfig(bytes.NewReader(data)) if err != nil || format != rasterFormats[contentType] { return 0, 0, image.ErrImageUnreadable } return config.Width, config.Height, nil } func hasSvgRoot(data []byte) bool { decoder := xml.NewDecoder(bytes.NewReader(data)) for { token, err := decoder.Token() if err != nil { return false } if start, ok := token.(xml.StartElement); ok { return start.Name.Local == svgRootElement } } }