2020-05-26 22:07:55 +02:00
// Vikunja is a to-do list application to facilitate your life.
2021-02-02 20:19:13 +01:00
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
2020-05-26 22:07:55 +02:00
//
// This program is free software: you can redistribute it and/or modify
2020-12-23 16:41:52 +01:00
// it under the terms of the GNU Affero General Public Licensee as published by
2020-05-26 22:07:55 +02:00
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2020-12-23 16:41:52 +01:00
// GNU Affero General Public Licensee for more details.
2020-05-26 22:07:55 +02:00
//
2020-12-23 16:41:52 +01:00
// You should have received a copy of the GNU Affero General Public Licensee
2020-05-26 22:07:55 +02:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package handler
import (
2021-12-20 19:42:02 +01:00
_ "image/gif" // To make sure the decoder used for generating blurHashes recognizes gifs
_ "image/jpeg" // To make sure the decoder used for generating blurHashes recognizes jpgs
_ "image/png" // To make sure the decoder used for generating blurHashes recognizes pngs
2022-08-15 23:37:05 +02:00
_ "golang.org/x/image/bmp" // To make sure the decoder used for generating blurHashes recognizes bmps
_ "golang.org/x/image/tiff" // To make sure the decoder used for generating blurHashes recognizes tiffs
_ "golang.org/x/image/webp" // To make sure the decoder used for generating blurHashes recognizes tiffs
"image"
2020-10-11 22:10:03 +02:00
"io"
"net/http"
"strconv"
"strings"
2020-12-23 16:32:28 +01:00
"code.vikunja.io/api/pkg/db"
2020-05-26 22:07:55 +02:00
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
2020-11-21 17:38:58 +01:00
auth2 "code.vikunja.io/api/pkg/modules/auth"
2020-05-26 22:07:55 +02:00
"code.vikunja.io/api/pkg/modules/background"
"code.vikunja.io/api/pkg/modules/background/unsplash"
2021-12-20 19:37:39 +01:00
"code.vikunja.io/api/pkg/modules/background/upload"
2020-06-11 19:31:37 +02:00
"code.vikunja.io/web"
2020-05-26 22:07:55 +02:00
"code.vikunja.io/web/handler"
2021-12-20 19:37:39 +01:00
"github.com/bbrks/go-blurhash"
2020-08-02 19:16:58 +02:00
"github.com/gabriel-vasile/mimetype"
2020-05-26 22:07:55 +02:00
"github.com/labstack/echo/v4"
2021-12-20 19:37:39 +01:00
"golang.org/x/image/draw"
"xorm.io/xorm"
2020-05-26 22:07:55 +02:00
)
// BackgroundProvider represents a thing which holds a background provider
// Lets us get a new fresh provider every time we need one.
type BackgroundProvider struct {
Provider func ( ) background . Provider
}
// SearchBackgrounds is the web handler to search for backgrounds
func ( bp * BackgroundProvider ) SearchBackgrounds ( c echo . Context ) error {
p := bp . Provider ( )
err := c . Bind ( p )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "No or invalid model provided: " + err . Error ( ) )
}
search := c . QueryParam ( "s" )
var page int64 = 1
pg := c . QueryParam ( "p" )
if pg != "" {
page , err = strconv . ParseInt ( pg , 10 , 64 )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Invalid page number: " + err . Error ( ) )
}
}
2020-12-23 16:32:28 +01:00
s := db . NewSession ( )
defer s . Close ( )
result , err := p . Search ( s , search , page )
2020-05-26 22:07:55 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
return echo . NewHTTPError ( http . StatusBadRequest , "An error occurred: " + err . Error ( ) )
}
if err := s . Commit ( ) ; err != nil {
_ = s . Rollback ( )
2020-05-26 22:07:55 +02:00
return echo . NewHTTPError ( http . StatusBadRequest , "An error occurred: " + err . Error ( ) )
}
return c . JSON ( http . StatusOK , result )
}
2020-06-11 19:31:37 +02:00
// This function does all kinds of preparations for setting and uploading a background
2020-12-23 16:32:28 +01:00
func ( bp * BackgroundProvider ) setBackgroundPreparations ( s * xorm . Session , c echo . Context ) ( list * models . List , auth web . Auth , err error ) {
2020-11-21 17:38:58 +01:00
auth , err = auth2 . GetAuthFromClaims ( c )
2020-05-26 22:07:55 +02:00
if err != nil {
2020-06-11 19:31:37 +02:00
return nil , nil , echo . NewHTTPError ( http . StatusBadRequest , "Invalid auth token: " + err . Error ( ) )
2020-05-26 22:07:55 +02:00
}
listID , err := strconv . ParseInt ( c . Param ( "list" ) , 10 , 64 )
if err != nil {
2020-06-11 19:31:37 +02:00
return nil , nil , echo . NewHTTPError ( http . StatusBadRequest , "Invalid list ID: " + err . Error ( ) )
2020-05-26 22:07:55 +02:00
}
// Check if the user has the right to change the list background
2020-06-11 19:31:37 +02:00
list = & models . List { ID : listID }
2020-12-23 16:32:28 +01:00
can , err := list . CanUpdate ( s , auth )
2020-05-26 22:07:55 +02:00
if err != nil {
2020-06-11 19:31:37 +02:00
return
2020-05-26 22:07:55 +02:00
}
if ! can {
log . Infof ( "Tried to update list background of list %d while not having the rights for it (User: %v)" , listID , auth )
2020-06-11 19:31:37 +02:00
return list , auth , models . ErrGenericForbidden { }
2020-05-26 22:07:55 +02:00
}
2020-05-29 22:12:16 +02:00
// Load the list
2020-12-23 16:32:28 +01:00
list , err = models . GetListSimpleByID ( s , list . ID )
2020-06-11 19:31:37 +02:00
return
}
// SetBackground sets an Image as list background
func ( bp * BackgroundProvider ) SetBackground ( c echo . Context ) error {
2020-12-23 16:32:28 +01:00
s := db . NewSession ( )
defer s . Close ( )
list , auth , err := bp . setBackgroundPreparations ( s , c )
2020-06-11 19:31:37 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-05-29 22:12:16 +02:00
return handler . HandleHTTPError ( err , c )
}
2020-05-26 22:07:55 +02:00
2020-06-11 19:31:37 +02:00
p := bp . Provider ( )
2020-05-26 22:07:55 +02:00
image := & background . Image { }
err = c . Bind ( image )
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-05-26 22:07:55 +02:00
return echo . NewHTTPError ( http . StatusBadRequest , "No or invalid model provided: " + err . Error ( ) )
}
2020-12-23 16:32:28 +01:00
err = p . Set ( s , image , list , auth )
2020-05-26 22:07:55 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-06-11 19:31:37 +02:00
return handler . HandleHTTPError ( err , c )
}
return c . JSON ( http . StatusOK , list )
}
2021-12-12 21:42:35 +01:00
func CreateBlurHash ( srcf io . Reader ) ( hash string , err error ) {
src , _ , err := image . Decode ( srcf )
if err != nil {
return "" , err
}
dst := image . NewRGBA ( image . Rect ( 0 , 0 , 32 , 32 ) )
draw . NearestNeighbor . Scale ( dst , dst . Rect , src , src . Bounds ( ) , draw . Over , nil )
return blurhash . Encode ( 4 , 3 , dst )
}
2020-06-11 19:31:37 +02:00
// UploadBackground uploads a background and passes the id of the uploaded file as an Image to the Set function of the BackgroundProvider.
func ( bp * BackgroundProvider ) UploadBackground ( c echo . Context ) error {
2020-12-23 16:32:28 +01:00
s := db . NewSession ( )
defer s . Close ( )
list , auth , err := bp . setBackgroundPreparations ( s , c )
2020-06-11 19:31:37 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-06-11 19:31:37 +02:00
return handler . HandleHTTPError ( err , c )
}
// Get + upload the image
file , err := c . FormFile ( "background" )
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-06-11 19:31:37 +02:00
return err
}
2021-12-12 21:42:35 +01:00
srcf , err := file . Open ( )
2020-06-11 19:31:37 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-06-11 19:31:37 +02:00
return err
}
2021-12-12 21:42:35 +01:00
defer srcf . Close ( )
2020-06-11 19:31:37 +02:00
2020-08-02 19:16:58 +02:00
// Validate we're dealing with an image
2021-12-12 21:42:35 +01:00
mime , err := mimetype . DetectReader ( srcf )
2020-08-02 19:16:58 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-08-02 19:16:58 +02:00
return handler . HandleHTTPError ( err , c )
}
if ! strings . HasPrefix ( mime . String ( ) , "image" ) {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-08-02 19:16:58 +02:00
return c . JSON ( http . StatusBadRequest , models . Message { Message : "Uploaded file is no image." } )
}
2021-12-20 19:37:39 +01:00
err = SaveBackgroundFile ( s , auth , list , srcf , file . Filename , uint64 ( file . Size ) )
2020-06-11 19:31:37 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-06-11 19:31:37 +02:00
if files . IsErrFileIsTooLarge ( err ) {
return echo . ErrBadRequest
}
return handler . HandleHTTPError ( err , c )
}
2021-12-20 19:37:39 +01:00
if err := s . Commit ( ) ; err != nil {
_ = s . Rollback ( )
return handler . HandleHTTPError ( err , c )
}
return c . JSON ( http . StatusOK , list )
}
func SaveBackgroundFile ( s * xorm . Session , auth web . Auth , list * models . List , srcf io . ReadSeeker , filename string , filesize uint64 ) ( err error ) {
_ , _ = srcf . Seek ( 0 , io . SeekStart )
f , err := files . Create ( srcf , filename , filesize , auth )
if err != nil {
return err
}
2021-12-12 21:42:35 +01:00
// Generate a blurHash
_ , _ = srcf . Seek ( 0 , io . SeekStart )
list . BackgroundBlurHash , err = CreateBlurHash ( srcf )
if err != nil {
2021-12-20 19:37:39 +01:00
return err
2021-12-12 21:42:35 +01:00
}
2020-06-11 19:31:37 +02:00
2021-12-12 21:42:35 +01:00
// Save it
2021-12-20 19:37:39 +01:00
p := upload . Provider { }
2021-12-12 21:42:35 +01:00
img := & background . Image { ID : strconv . FormatInt ( f . ID , 10 ) }
err = p . Set ( s , img , list , auth )
2021-12-20 19:37:39 +01:00
return err
2020-05-26 22:07:55 +02:00
}
2021-03-21 17:49:14 +01:00
func checkListBackgroundRights ( s * xorm . Session , c echo . Context ) ( list * models . List , auth web . Auth , err error ) {
auth , err = auth2 . GetAuthFromClaims ( c )
if err != nil {
return nil , auth , echo . NewHTTPError ( http . StatusBadRequest , "Invalid auth token: " + err . Error ( ) )
}
listID , err := strconv . ParseInt ( c . Param ( "list" ) , 10 , 64 )
if err != nil {
return nil , auth , echo . NewHTTPError ( http . StatusBadRequest , "Invalid list ID: " + err . Error ( ) )
}
// Check if a background for this list exists + Rights
list = & models . List { ID : listID }
can , _ , err := list . CanRead ( s , auth )
if err != nil {
_ = s . Rollback ( )
return nil , auth , handler . HandleHTTPError ( err , c )
}
if ! can {
_ = s . Rollback ( )
log . Infof ( "Tried to get list background of list %d while not having the rights for it (User: %v)" , listID , auth )
return nil , auth , echo . NewHTTPError ( http . StatusForbidden )
}
return
}
2020-05-26 22:07:55 +02:00
// GetListBackground serves a previously set background from a list
// It has no knowledge of the provider that was responsible for setting the background.
// @Summary Get the list background
// @Description Get the list background of a specific list. **Returns json on error.**
// @tags list
// @Produce octet-stream
// @Param id path int true "List ID"
// @Security JWTKeyAuth
// @Success 200 {} string "The list background file."
// @Failure 403 {object} models.Message "No access to this list."
// @Failure 404 {object} models.Message "The list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/background [get]
func GetListBackground ( c echo . Context ) error {
2020-12-23 16:32:28 +01:00
s := db . NewSession ( )
defer s . Close ( )
2021-03-21 17:49:14 +01:00
list , _ , err := checkListBackgroundRights ( s , c )
2020-05-26 22:07:55 +02:00
if err != nil {
2021-03-21 17:49:14 +01:00
return err
2020-05-26 22:07:55 +02:00
}
2021-03-21 17:49:14 +01:00
2020-05-26 22:07:55 +02:00
if list . BackgroundFileID == 0 {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-05-26 22:07:55 +02:00
return echo . NotFoundHandler ( c )
}
// Get the file
bgFile := & files . File {
ID : list . BackgroundFileID ,
}
if err := bgFile . LoadFileByID ( ) ; err != nil {
2020-12-23 16:32:28 +01:00
_ = s . Rollback ( )
2020-05-26 22:07:55 +02:00
return handler . HandleHTTPError ( err , c )
}
// Unsplash requires pingbacks as per their api usage guidelines.
// To do this in a privacy-preserving manner, we do the ping from inside of Vikunja to not expose any user details.
// FIXME: This should use an event once we have events
2020-12-23 16:32:28 +01:00
unsplash . Pingback ( s , bgFile )
if err := s . Commit ( ) ; err != nil {
_ = s . Rollback ( )
return handler . HandleHTTPError ( err , c )
}
2020-05-26 22:07:55 +02:00
// Serve the file
return c . Stream ( http . StatusOK , "image/jpg" , bgFile . File )
}
2021-03-21 17:49:14 +01:00
// RemoveListBackground removes a list background, no matter the background provider
// @Summary Remove a list background
// @Description Removes a previously set list background, regardless of the list provider used to set the background. It does not throw an error if the list does not have a background.
// @tags list
// @Produce json
// @Param id path int true "List ID"
// @Security JWTKeyAuth
// @Success 200 {object} models.List "The list"
// @Failure 403 {object} models.Message "No access to this list."
// @Failure 404 {object} models.Message "The list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/background [delete]
func RemoveListBackground ( c echo . Context ) error {
s := db . NewSession ( )
defer s . Close ( )
list , auth , err := checkListBackgroundRights ( s , c )
if err != nil {
return err
}
list . BackgroundFileID = 0
list . BackgroundInformation = nil
2021-12-12 21:42:35 +01:00
list . BackgroundBlurHash = ""
2021-11-13 17:52:14 +01:00
err = models . UpdateList ( s , list , auth , true )
2021-03-21 17:49:14 +01:00
if err != nil {
return err
}
return c . JSON ( http . StatusOK , list )
}