...

Source file src/codeberg.org/tslocum/sriracha/internal/server/template.go

Documentation: codeberg.org/tslocum/sriracha/internal/server

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"embed"
     6  	"fmt"
     7  	"html/template"
     8  	"io"
     9  	"log"
    10  	"maps"
    11  	"math/rand"
    12  	"net/http"
    13  	"net/url"
    14  	"path/filepath"
    15  	"slices"
    16  	"strings"
    17  
    18  	. "codeberg.org/tslocum/sriracha/model"
    19  	"github.com/leonelquinteros/gotext"
    20  )
    21  
    22  //go:embed template
    23  var templateFS embed.FS
    24  
    25  type manageData struct {
    26  	Account  *Account
    27  	Accounts []*Account
    28  	Ban      *Ban
    29  	Bans     []*Ban
    30  	Banner   *Banner
    31  	Banners  []*Banner
    32  	Board    *Board
    33  	Boards   []*Board
    34  	Category *Category
    35  	Keyword  *Keyword
    36  	Keywords []*Keyword
    37  	Log      *Log
    38  	Logs     []*Log
    39  	News     *News
    40  	AllNews  []*News
    41  	Page     *Page
    42  	Pages    []*Page
    43  	Plugin   *pluginInfo
    44  	Plugins  []*pluginInfo
    45  	Report   *Report
    46  	Reports  []*Report
    47  }
    48  
    49  type templateData struct {
    50  	Account       *Account
    51  	Info          string
    52  	Message       template.HTML
    53  	Message2      template.HTML
    54  	Message3      template.HTML
    55  	Board         *Board
    56  	Boards        []*Board
    57  	Categories    []*Category
    58  	News          *News
    59  	AllNews       []*News
    60  	Subscriptions []*Subscription
    61  	Page          int
    62  	Pages         int
    63  	Post          *Post
    64  	Threads       [][]*Post
    65  	ReplyMode     int
    66  	ModMode       bool
    67  	Extra         string
    68  	Extra2        string
    69  	Extra3        string
    70  	Opt           *ServerOptions
    71  	Manage        *manageData
    72  	Template      string
    73  
    74  	// Calculated fields.
    75  	IndexBoards []*Board
    76  	tpl         *template.Template
    77  	buf         *bytes.Buffer
    78  }
    79  
    80  func (data *templateData) Style() string {
    81  	switch {
    82  	case data.Account != nil:
    83  		return data.Account.Style
    84  	case data.Board != nil:
    85  		return data.Board.Style
    86  	default:
    87  		return ""
    88  	}
    89  }
    90  
    91  func (data *templateData) ManageMode() bool {
    92  	return strings.HasPrefix(data.Template, "manage_")
    93  }
    94  
    95  func (data *templateData) GuideLink() string {
    96  	return `<a href="/guide.html" target="_blank">` + Get(data.Board, data.Account, "visitor guide") + `</a>`
    97  }
    98  
    99  func (data *templateData) BoardError(w http.ResponseWriter, message string) {
   100  	data.Template = "board_error"
   101  	data.Info = message
   102  	data.execute(w)
   103  }
   104  
   105  func (data *templateData) ManageError(message string) {
   106  	data.Template = "manage_error"
   107  	data.Info = message
   108  }
   109  
   110  func (data *templateData) forbidden(w http.ResponseWriter, required AccountRole) bool {
   111  	allow := required != 0 && data.Account != nil && data.Account.Role != 0 && data.Account.Role <= required
   112  	if allow {
   113  		return false
   114  	}
   115  	data.Template = "manage_error"
   116  	data.Info = "Access forbidden."
   117  	return true
   118  }
   119  
   120  func (data *templateData) Redirect(w http.ResponseWriter, r *http.Request, destination string) {
   121  	data.Template = ""
   122  	http.Redirect(w, r, destination, http.StatusFound)
   123  }
   124  
   125  func (data *templateData) executeWithError(w io.Writer) error {
   126  	if data.Template == "" {
   127  		return nil
   128  	}
   129  
   130  	if data.Account != nil {
   131  		data.IndexBoards = data.Boards
   132  	} else {
   133  		data.IndexBoards = data.IndexBoards[:0]
   134  		for _, b := range data.Boards {
   135  			if b.Hide == HideIndex || b.Hide == HideEverywhere {
   136  				continue
   137  			}
   138  			data.IndexBoards = append(data.IndexBoards, b)
   139  		}
   140  	}
   141  
   142  	var boardTemplate bool
   143  	if strings.HasPrefix(data.Template, "board_") {
   144  		prefix := "imgboard_"
   145  		if data.Board != nil && data.Board.Type == TypeForum {
   146  			prefix = "forum_"
   147  		}
   148  		data.Template = prefix + strings.TrimPrefix(data.Template, "board_")
   149  		boardTemplate = true
   150  	}
   151  
   152  	responseWriter, ok := w.(http.ResponseWriter)
   153  	if ok {
   154  		responseWriter.Header().Set("Content-Type", "text/html")
   155  	}
   156  
   157  	var funcMap template.FuncMap
   158  	if strings.HasPrefix(data.Template, "manage_") && data.Account != nil && data.Account.Locale != "" {
   159  		funcMap = templateFuncMaps[data.Account.Locale]
   160  	} else if boardTemplate {
   161  		var locale string
   162  		if data.Account != nil {
   163  			locale = data.Account.Locale
   164  		} else if data.Board != nil {
   165  			locale = data.Board.Locale
   166  		}
   167  		funcMap = templateFuncMaps[locale]
   168  	}
   169  	if funcMap == nil {
   170  		funcMap = templateFuncMaps[""]
   171  	}
   172  
   173  	data.buf.Reset()
   174  
   175  	tplName := data.Template + ".gohtml"
   176  	if data.Template == "line" {
   177  		tplName = data.Template
   178  	}
   179  	err := data.tpl.Funcs(funcMap).ExecuteTemplate(data.buf, tplName, data)
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	io.Copy(w, data.buf)
   185  	return nil
   186  }
   187  
   188  func (data *templateData) execute(w io.Writer) {
   189  	err := data.executeWithError(w)
   190  	if err != nil {
   191  		log.Fatal(err)
   192  	}
   193  }
   194  
   195  var expandableMedia = []string{".bmp", ".gif", ".jpg", ".png", ".svg", ".tif"}
   196  
   197  var templateFuncMap = template.FuncMap{
   198  	"Banner": func(banners []*Banner) *Banner {
   199  		l := len(banners)
   200  		switch l {
   201  		case 0:
   202  			return nil
   203  		case 1:
   204  			return banners[0]
   205  		default:
   206  			return banners[rand.Intn(l)]
   207  		}
   208  	},
   209  	"Contains": strings.Contains,
   210  	"Format": func(text string) template.HTML {
   211  		return template.HTML(strings.ReplaceAll(text, "\n", "<br>\n"))
   212  	},
   213  	"GetBoard": func(boardID int, boards []*Board) *Board {
   214  		for _, board := range boards {
   215  			if board.ID == boardID {
   216  				return board
   217  			}
   218  		}
   219  		return nil
   220  	},
   221  	"HasExpandableMedia": func(thread []*Post) bool {
   222  		for _, p := range thread {
   223  			if p.File != "" && !p.IsEmbed() && slices.Contains(expandableMedia, filepath.Ext(p.File)) {
   224  				return true
   225  			}
   226  		}
   227  		return false
   228  	},
   229  	"HasPrefix": strings.HasPrefix,
   230  	"HasSuffix": strings.HasSuffix,
   231  	"HTML": func(text string) template.HTML {
   232  		return template.HTML(text)
   233  	},
   234  	"Iterate": func(i int) []int {
   235  		var values []int
   236  		for v := 0; v <= i; v++ {
   237  			values = append(values, v)
   238  		}
   239  		return values
   240  	},
   241  	"May": func(action string, account *Account, access map[string]string) bool {
   242  		var required AccountRole
   243  		switch access[action] {
   244  		case "mod":
   245  			required = RoleMod
   246  		case "admin":
   247  			required = RoleAdmin
   248  		case "super-admin":
   249  			required = RoleSuperAdmin
   250  		default:
   251  			return false
   252  		}
   253  		return account != nil && account.Role <= required
   254  	},
   255  	"MinusOne": func(i int) int {
   256  		return i - 1
   257  	},
   258  	"Omitted": func(showReplies int, numReplies int) int {
   259  		if showReplies == 0 {
   260  			return numReplies
   261  		} else if numReplies <= showReplies {
   262  			return 0
   263  		}
   264  		return numReplies - showReplies
   265  	},
   266  	"PlusOne": func(i int) int {
   267  		return i + 1
   268  	},
   269  	"ShowReply": func(showReplies int, threadPosts int, postIndex int) bool {
   270  		if showReplies == 0 {
   271  			return true
   272  		}
   273  		return postIndex >= threadPosts-showReplies
   274  	},
   275  	"Slice": func(elements ...any) []any {
   276  		return elements
   277  	},
   278  	"Sprintf": fmt.Sprintf,
   279  	"ToUpper": strings.ToUpper,
   280  	"ToLower": strings.ToLower,
   281  	"Title":   strings.Title,
   282  	"UnderscoreTitle": func(text string) string {
   283  		return strings.Title(strings.ReplaceAll(text, "_", " "))
   284  	},
   285  	"URLEscape": func(text string) string {
   286  		return url.PathEscape(text)
   287  	},
   288  	"ZeroPadTo3": func(i int) string {
   289  		return fmt.Sprintf("%03d", i)
   290  	},
   291  }
   292  
   293  var templateFuncMaps map[string]template.FuncMap
   294  
   295  func newTemplateFuncMap(locale string) template.FuncMap {
   296  	f := make(template.FuncMap)
   297  	maps.Copy(f, templateFuncMap)
   298  
   299  	domain := "sriracha"
   300  	if locale != "" {
   301  		domain += "-" + locale
   302  	}
   303  	f["T"] = func(message string, vars ...interface{}) string {
   304  		return gotext.GetD(domain, message, vars...)
   305  	}
   306  	f["TN"] = func(singular string, plural string, n int, vars ...interface{}) string {
   307  		return gotext.GetND(domain, singular, plural, n, vars...)
   308  	}
   309  	return f
   310  }
   311  
   312  func (s *Server) newTemplateData() *templateData {
   313  	const initialBufferSize = 128000 // 128 Kilobytes.
   314  	writeBuf := bytes.NewBuffer(make([]byte, initialBufferSize))
   315  	return &templateData{
   316  		Manage: &manageData{
   317  			Plugins: allPluginInfo,
   318  		},
   319  		Opt: &s.opt,
   320  		tpl: s.tpl,
   321  		buf: writeBuf,
   322  	}
   323  }
   324  

View as plain text