/ *
Copyright 2017 - 2022 CAcert Inc .
SPDX - License - Identifier : Apache - 2.0
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package main
import (
"bytes"
"database/sql"
"errors"
"fmt"
"html/template"
"io/fs"
"net/http"
"path/filepath"
"strings"
"time"
"git.cacert.org/cacert-boardvoting/internal/models"
"git.cacert.org/cacert-boardvoting/internal/validator"
"git.cacert.org/cacert-boardvoting/ui"
"github.com/Masterminds/sprig/v3"
"github.com/gorilla/csrf"
"github.com/julienschmidt/httprouter"
)
func newTemplateCache ( ) ( map [ string ] * template . Template , error ) {
cache := map [ string ] * template . Template { }
pages , err := fs . Glob ( ui . Files , "html/pages/*.html" )
if err != nil {
return nil , fmt . Errorf ( "could not find page templates: %w" , err )
}
funcMaps := sprig . FuncMap ( )
funcMaps [ "nl2br" ] = func ( text string ) template . HTML {
// #nosec G203 input is sanitized
return template . HTML ( strings . ReplaceAll ( template . HTMLEscapeString ( text ) , "\n" , "<br>" ) )
}
funcMaps [ "canManageUsers" ] = func ( * models . Voter ) bool {
return false
}
funcMaps [ csrf . TemplateTag ] = csrf . TemplateField
for _ , page := range pages {
name := filepath . Base ( page )
ts , err := template . New ( "" ) . Funcs ( funcMaps ) . ParseFS (
ui . Files ,
"html/base.html" ,
"html/partials/*.html" ,
page ,
)
if err != nil {
return nil , fmt . Errorf ( "could not parse base template: %w" , err )
}
cache [ name ] = ts
}
return cache , nil
}
type templateData struct {
PrevPage string
NextPage string
Voter * models . Voter
Motion * models . MotionForDisplay
Motions [ ] * models . MotionForDisplay
Request * http . Request
Flashes [ ] string
Form any
Params struct {
Flags struct {
Unvoted bool
}
}
}
func ( app * application ) newTemplateData ( r * http . Request ) * templateData {
return & templateData {
Request : r ,
}
}
func ( app * application ) render ( w http . ResponseWriter , status int , page string , data * templateData ) {
ts , ok := app . templateCache [ page ]
if ! ok {
app . serverError ( w , fmt . Errorf ( "the template %s does not exist" , page ) )
return
}
buf := new ( bytes . Buffer )
err := ts . ExecuteTemplate ( buf , "base" , data )
if err != nil {
app . serverError ( w , err )
return
}
w . WriteHeader ( status )
_ , _ = buf . WriteTo ( w )
}
func ( m * templateData ) setPaginationParameters ( first , last * time . Time ) error {
motions := m . Motions
if len ( motions ) > 0 && first . Before ( motions [ len ( motions ) - 1 ] . Proposed ) {
marshalled , err := motions [ len ( motions ) - 1 ] . Proposed . MarshalText ( )
if err != nil {
return fmt . Errorf ( "could not serialize timestamp: %w" , err )
}
m . NextPage = string ( marshalled )
}
if len ( motions ) > 0 && last . After ( motions [ 0 ] . Proposed ) {
marshalled , err := motions [ 0 ] . Proposed . MarshalText ( )
if err != nil {
return fmt . Errorf ( "could not serialize timestamp: %w" , err )
}
m . PrevPage = string ( marshalled )
}
return nil
}
func ( app * application ) motionList ( w http . ResponseWriter , r * http . Request ) {
if r . URL . Path != "/motions/" {
app . notFound ( w )
return
}
var (
listOptions * models . MotionListOptions
err error
)
listOptions , err = calculateMotionListOptions ( r )
if err != nil {
app . clientError ( w , http . StatusBadRequest )
return
}
ctx := r . Context ( )
motions , err := app . motions . GetMotions ( ctx , listOptions )
if err != nil {
app . serverError ( w , err )
return
}
first , last , err := app . motions . TimestampRange ( ctx )
if err != nil {
app . serverError ( w , err )
return
}
templateData := app . newTemplateData ( r )
templateData . Motions = motions
err = templateData . setPaginationParameters ( first , last )
if err != nil {
app . serverError ( w , err )
return
}
app . render ( w , http . StatusOK , "motions.html" , templateData )
}
func calculateMotionListOptions ( r * http . Request ) ( * models . MotionListOptions , error ) {
const (
queryParamBefore = "before"
queryParamAfter = "after"
motionsPerPage = 10
)
listOptions := & models . MotionListOptions { Limit : motionsPerPage }
if r . URL . Query ( ) . Has ( queryParamAfter ) {
var after time . Time
err := after . UnmarshalText ( [ ] byte ( r . URL . Query ( ) . Get ( queryParamAfter ) ) )
if err != nil {
return nil , fmt . Errorf ( "could not unmarshal timestamp: %w" , err )
}
listOptions . After = & after
} else if r . URL . Query ( ) . Has ( queryParamBefore ) {
var before time . Time
err := before . UnmarshalText ( [ ] byte ( r . URL . Query ( ) . Get ( queryParamBefore ) ) )
if err != nil {
return nil , fmt . Errorf ( "could not unmarshal timestamp: %w" , err )
}
listOptions . Before = & before
}
return listOptions , nil
}
func ( app * application ) motionDetails ( w http . ResponseWriter , r * http . Request ) {
params := httprouter . ParamsFromContext ( r . Context ( ) )
tag := params . ByName ( "tag" )
showVotes := r . URL . Query ( ) . Has ( "showvotes" )
motion , err := app . motions . GetMotionByTag ( r . Context ( ) , tag , showVotes )
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
app . serverError ( w , err )
return
}
if motion == nil {
app . notFound ( w )
return
}
data := app . newTemplateData ( r )
data . Motion = motion
app . render ( w , http . StatusOK , "motion.html" , data )
}
type NewMotionForm struct {
Title string ` form:"title" `
Content string ` form:"content" `
Type * models . VoteType ` form:"type" `
Due string ` form:"due" `
Voter * models . Voter ` form:"-" `
validator . Validator ` form:"-" `
}
func ( app * application ) newMotionForm ( w http . ResponseWriter , r * http . Request ) {
data := app . newTemplateData ( r )
data . Form = & NewMotionForm {
Voter : & models . Voter { } ,
Type : models . VoteTypeMotion ,
}
app . render ( w , http . StatusOK , "create_motion.html" , data )
}
func ( app * application ) newMotionSubmit ( w http . ResponseWriter , r * http . Request ) {
const (
minimumTitleLength = 3
maximumTitleLength = 200
minimumContentLength = 3
maximumContentLength = 8000
)
err := r . ParseForm ( )
if err != nil {
app . clientError ( w , http . StatusBadRequest )
return
}
var form NewMotionForm
err = app . formDecoder . Decode ( & form , r . PostForm )
if err != nil {
app . errorLog . Printf ( "form decoding failed: %v" , err )
app . clientError ( w , http . StatusBadRequest )
return
}
form . CheckField ( validator . NotBlank ( form . Title ) , "title" , "This field cannot be blank" )
form . CheckField ( validator . MinChars ( form . Title , minimumTitleLength ) , "title" , fmt . Sprintf ( "This field must be at least %d characters long" , minimumTitleLength ) )
form . CheckField ( validator . MaxChars ( form . Title , maximumTitleLength ) , "title" , fmt . Sprintf ( "This field must be at most %d characters long" , maximumTitleLength ) )
form . CheckField ( validator . NotBlank ( form . Content ) , "content" , "This field cannot be blank" )
form . CheckField ( validator . MinChars ( form . Content , minimumContentLength ) , "content" , fmt . Sprintf ( "This field must be at least %d characters long" , minimumContentLength ) )
form . CheckField ( validator . MaxChars ( form . Content , maximumContentLength ) , "content" , fmt . Sprintf ( "This field must be at most %d characters long" , maximumContentLength ) )
form . CheckField ( validator . PermittedStr (
form . Due ,
"+3 days" ,
"+7 days" ,
"+14 days" ,
"+28 days" ,
) , "due" , "invalid duration choice" )
if ! form . Valid ( ) {
form . Voter = & models . Voter { }
data := app . newTemplateData ( r )
data . Form = form
app . render ( w , http . StatusUnprocessableEntity , "create_motion.html" , data )
return
}
w . WriteHeader ( http . StatusOK )
_ , _ = w . Write ( [ ] byte ( fmt . Sprintf ( "%+v" , form ) ) )
// TODO: insert motion
// TODO: redirect to motion detail page
}
func ( app * application ) voteForm ( writer http . ResponseWriter , request * http . Request ) {
panic ( "not implemented" )
}
func ( app * application ) voteSubmit ( writer http . ResponseWriter , request * http . Request ) {
panic ( "not implemented" )
}
func ( app * application ) proxyVoteForm ( writer http . ResponseWriter , request * http . Request ) {
panic ( "not implemented" )
}
func ( app * application ) proxyVoteSubmit ( writer http . ResponseWriter , request * http . Request ) {
panic ( "not implemented" )
}