From f7ded3d432d6d54873eb19e75f1d21ea3b613251 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 2 Sep 2019 21:16:30 -0600
Subject: [PATCH] Add option to serve quarantine thumbnail for media downloads

Fixes https://github.com/turt2live/matrix-media-repo/issues/182
---
 common/config/config.go                       |  3 +
 config.sample.yaml                            |  6 ++
 .../download_controller.go                    | 27 ++++++
 .../quarantine_controller.go                  | 89 +++++++++++++++++++
 .../thumbnail_controller.go                   | 79 +---------------
 5 files changed, 127 insertions(+), 77 deletions(-)
 create mode 100644 controllers/quarantine_controller/quarantine_controller.go

diff --git a/common/config/config.go b/common/config/config.go
index a1a8fc99..b56ad892 100644
--- a/common/config/config.go
+++ b/common/config/config.go
@@ -113,6 +113,7 @@ type CacheConfig struct {
 
 type QuarantineConfig struct {
 	ReplaceThumbnails bool   `yaml:"replaceThumbnails"`
+	ReplaceDownloads  bool   `yaml:"replaceDownloads"`
 	ThumbnailPath     string `yaml:"thumbnailPath"`
 	AllowLocalAdmins  bool   `yaml:"allowLocalAdmins"`
 }
@@ -308,7 +309,9 @@ func NewDefaultConfig() *MediaRepoConfig {
 		},
 		Quarantine: &QuarantineConfig{
 			ReplaceThumbnails: true,
+			ReplaceDownloads:  false,
 			ThumbnailPath:     "",
+			AllowLocalAdmins:  true,
 		},
 		TimeoutSeconds: &TimeoutsConfig{
 			UrlPreviews:  10,
diff --git a/config.sample.yaml b/config.sample.yaml
index bbb83aa3..b1a21f7d 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -288,6 +288,12 @@ quarantine:
   # not affect regular downloads of files.
   replaceThumbnails: true
 
+  # If true, when media which has been quarantined is requested an image will be returned. If
+  # no image is given in the thumbnailPath below then a generated image will be provided. This
+  # will replace media which is not an image (ie: quarantining a PDF will replace the PDF with
+  # an image).
+  replaceDownloads: false
+
   # If provided, the given image will be returned as a thumbnail for media that is quarantined.
   #thumbnailPath: "/path/to/thumbnail.png"
 
diff --git a/controllers/download_controller/download_controller.go b/controllers/download_controller/download_controller.go
index 4cf85eec..7ea4acb3 100644
--- a/controllers/download_controller/download_controller.go
+++ b/controllers/download_controller/download_controller.go
@@ -1,16 +1,20 @@
 package download_controller
 
 import (
+	"bytes"
 	"context"
 	"database/sql"
 	"errors"
 	"fmt"
 	"time"
 
+	"github.com/disintegration/imaging"
 	"github.com/patrickmn/go-cache"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common"
+	"github.com/turt2live/matrix-media-repo/common/config"
 	"github.com/turt2live/matrix-media-repo/common/globals"
+	"github.com/turt2live/matrix-media-repo/controllers/quarantine_controller"
 	"github.com/turt2live/matrix-media-repo/internal_cache"
 	"github.com/turt2live/matrix-media-repo/storage"
 	"github.com/turt2live/matrix-media-repo/storage/datastore"
@@ -64,6 +68,29 @@ func GetMedia(origin string, mediaId string, downloadRemote bool, blockForMedia
 		if media != nil {
 			if media.Quarantined {
 				log.Warn("Quarantined media accessed")
+
+				if config.Get().Quarantine.ReplaceDownloads {
+					log.Info("Replacing thumbnail with a quarantined one")
+
+					img, err := quarantine_controller.GenerateQuarantineThumbnail(512, 512)
+					if err != nil {
+						return nil, err
+					}
+
+					data := &bytes.Buffer{}
+					imaging.Encode(data, img, imaging.PNG)
+					return &types.MinimalMedia{
+						// Lie about all the details
+						Stream:      util.BufferToStream(data),
+						ContentType: "image/png",
+						UploadName:  "quarantine.png",
+						SizeBytes:   int64(data.Len()),
+						MediaId:     mediaId,
+						Origin:      origin,
+						KnownMedia:  media,
+					}, nil
+				}
+
 				return nil, common.ErrMediaQuarantined
 			}
 
diff --git a/controllers/quarantine_controller/quarantine_controller.go b/controllers/quarantine_controller/quarantine_controller.go
new file mode 100644
index 00000000..d757428e
--- /dev/null
+++ b/controllers/quarantine_controller/quarantine_controller.go
@@ -0,0 +1,89 @@
+package quarantine_controller
+
+import (
+	"bytes"
+	"image"
+	"image/color"
+	"math"
+
+	"github.com/disintegration/imaging"
+	"github.com/fogleman/gg"
+	"github.com/golang/freetype/truetype"
+	"github.com/turt2live/matrix-media-repo/common/config"
+	"golang.org/x/image/font/gofont/gosmallcaps"
+)
+
+func GenerateQuarantineThumbnail(width int, height int) (image.Image, error) {
+	var centerImage image.Image
+	var err error
+	if config.Get().Quarantine.ThumbnailPath != "" {
+		centerImage, err = imaging.Open(config.Get().Quarantine.ThumbnailPath)
+	} else {
+		centerImage, err = generateDefaultQuarantineThumbnail()
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	c := gg.NewContext(width, height)
+
+	centerImage = imaging.Fit(centerImage, width, height, imaging.Lanczos)
+
+	c.DrawImageAnchored(centerImage, width/2, height/2, 0.5, 0.5)
+
+	buf := &bytes.Buffer{}
+	c.EncodePNG(buf)
+
+	return imaging.Decode(buf)
+}
+
+func generateDefaultQuarantineThumbnail() (image.Image, error) {
+	c := gg.NewContext(700, 700)
+	c.Clear()
+
+	red := color.RGBA{R: 190, G: 26, B: 25, A: 255}
+	orange := color.RGBA{R: 255, G: 186, B: 73, A: 255}
+	x := 350.0
+	y := 300.0
+	r := 256.0
+	w := 55.0
+	p := 64.0
+	m := "media not allowed"
+
+	c.SetColor(orange)
+	c.DrawRectangle(0, 0, 700, 700)
+	c.Fill()
+
+	c.SetColor(red)
+	c.DrawCircle(x, y, r)
+	c.Fill()
+
+	c.SetColor(color.White)
+	c.DrawCircle(x, y, r-w)
+	c.Fill()
+
+	lr := r - (w / 2)
+	sx := x + (lr * math.Cos(gg.Radians(225.0)))
+	sy := y + (lr * math.Sin(gg.Radians(225.0)))
+	ex := x + (lr * math.Cos(gg.Radians(45.0)))
+	ey := y + (lr * math.Sin(gg.Radians(45.0)))
+	c.SetLineCap(gg.LineCapButt)
+	c.SetLineWidth(w)
+	c.SetColor(red)
+	c.DrawLine(sx, sy, ex, ey)
+	c.Stroke()
+
+	f, err := truetype.Parse(gosmallcaps.TTF)
+	if err != nil {
+		panic(err)
+	}
+
+	c.SetColor(color.Black)
+	c.SetFontFace(truetype.NewFace(f, &truetype.Options{Size: 64}))
+	c.DrawStringAnchored(m, x, y+r+p, 0.5, 0.5)
+
+	buf := &bytes.Buffer{}
+	c.EncodePNG(buf)
+
+	return imaging.Decode(buf)
+}
diff --git a/controllers/thumbnail_controller/thumbnail_controller.go b/controllers/thumbnail_controller/thumbnail_controller.go
index e91f954a..72c65b3d 100644
--- a/controllers/thumbnail_controller/thumbnail_controller.go
+++ b/controllers/thumbnail_controller/thumbnail_controller.go
@@ -5,14 +5,9 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
-	"image"
-	"image/color"
-	"math"
 	"time"
 
 	"github.com/disintegration/imaging"
-	"github.com/fogleman/gg"
-	"github.com/golang/freetype/truetype"
 	"github.com/patrickmn/go-cache"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
@@ -20,12 +15,12 @@ import (
 	"github.com/turt2live/matrix-media-repo/common/config"
 	"github.com/turt2live/matrix-media-repo/common/globals"
 	"github.com/turt2live/matrix-media-repo/controllers/download_controller"
+	"github.com/turt2live/matrix-media-repo/controllers/quarantine_controller"
 	"github.com/turt2live/matrix-media-repo/internal_cache"
 	"github.com/turt2live/matrix-media-repo/storage"
 	"github.com/turt2live/matrix-media-repo/storage/datastore"
 	"github.com/turt2live/matrix-media-repo/types"
 	"github.com/turt2live/matrix-media-repo/util"
-	"golang.org/x/image/font/gofont/gosmallcaps"
 )
 
 // These are the content types that we can actually thumbnail
@@ -66,7 +61,7 @@ func GetThumbnail(origin string, mediaId string, desiredWidth int, desiredHeight
 		if config.Get().Quarantine.ReplaceThumbnails {
 			log.Info("Replacing thumbnail with a quarantined one")
 
-			img, err := GenerateQuarantineThumbnail(desiredWidth, desiredHeight)
+			img, err := quarantine_controller.GenerateQuarantineThumbnail(desiredWidth, desiredHeight)
 			if err != nil {
 				return nil, err
 			}
@@ -221,76 +216,6 @@ func GetOrGenerateThumbnail(media *types.Media, width int, height int, animated
 	return result.thumbnail, result.err
 }
 
-func GenerateQuarantineThumbnail(width int, height int) (image.Image, error) {
-	var centerImage image.Image
-	var err error
-	if config.Get().Quarantine.ThumbnailPath != "" {
-		centerImage, err = imaging.Open(config.Get().Quarantine.ThumbnailPath)
-	} else {
-		centerImage, err = generateDefaultQuarantineThumbnail()
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	c := gg.NewContext(width, height)
-
-	centerImage = imaging.Fit(centerImage, width, height, imaging.Lanczos)
-
-	c.DrawImageAnchored(centerImage, width/2, height/2, 0.5, 0.5)
-
-	buf := &bytes.Buffer{}
-	c.EncodePNG(buf)
-
-	return imaging.Decode(buf)
-}
-
-func generateDefaultQuarantineThumbnail() (image.Image, error) {
-	c := gg.NewContext(700, 700)
-	c.Clear()
-
-	red := color.RGBA{R: 190, G: 26, B: 25, A: 255}
-	x := 350.0
-	y := 300.0
-	r := 256.0
-	w := 55.0
-	p := 64.0
-	m := "media not allowed"
-
-	c.SetColor(red)
-	c.DrawCircle(x, y, r)
-	c.Fill()
-
-	c.SetColor(color.White)
-	c.DrawCircle(x, y, r-w)
-	c.Fill()
-
-	lr := r - (w / 2)
-	sx := x + (lr * math.Cos(gg.Radians(225.0)))
-	sy := y + (lr * math.Sin(gg.Radians(225.0)))
-	ex := x + (lr * math.Cos(gg.Radians(45.0)))
-	ey := y + (lr * math.Sin(gg.Radians(45.0)))
-	c.SetLineCap(gg.LineCapButt)
-	c.SetLineWidth(w)
-	c.SetColor(red)
-	c.DrawLine(sx, sy, ex, ey)
-	c.Stroke()
-
-	f, err := truetype.Parse(gosmallcaps.TTF)
-	if err != nil {
-		panic(err)
-	}
-
-	c.SetColor(color.Black)
-	c.SetFontFace(truetype.NewFace(f, &truetype.Options{Size: 64}))
-	c.DrawStringAnchored(m, x, y+r+p, 0.5, 0.5)
-
-	buf := &bytes.Buffer{}
-	c.EncodePNG(buf)
-
-	return imaging.Decode(buf)
-}
-
 func pickThumbnailDimensions(desiredWidth int, desiredHeight int, desiredMethod string) (int, int, string, error) {
 	if desiredWidth <= 0 {
 		return 0, 0, "", errors.New("width must be positive")
-- 
GitLab