template.go (5118B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package router 19 20 import ( 21 "fmt" 22 "html/template" 23 "os" 24 "path/filepath" 25 "strings" 26 "time" 27 28 "github.com/gin-gonic/gin" 29 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 30 "github.com/superseriousbusiness/gotosocial/internal/config" 31 "github.com/superseriousbusiness/gotosocial/internal/log" 32 "github.com/superseriousbusiness/gotosocial/internal/text" 33 "github.com/superseriousbusiness/gotosocial/internal/util" 34 ) 35 36 const ( 37 justTime = "15:04" 38 dateYear = "Jan 02, 2006" 39 dateTime = "Jan 02, 15:04" 40 dateYearTime = "Jan 02, 2006, 15:04" 41 monthYear = "Jan, 2006" 42 badTimestamp = "bad timestamp" 43 ) 44 45 // LoadTemplates loads html templates for use by the given engine 46 func LoadTemplates(engine *gin.Engine) error { 47 templateBaseDir := config.GetWebTemplateBaseDir() 48 if templateBaseDir == "" { 49 return fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebTemplateBaseDirFlag()) 50 } 51 52 templateBaseDir, err := filepath.Abs(templateBaseDir) 53 if err != nil { 54 return fmt.Errorf("error getting absolute path of %s: %s", templateBaseDir, err) 55 } 56 57 if _, err := os.Stat(filepath.Join(templateBaseDir, "index.tmpl")); err != nil { 58 return fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err) 59 } 60 61 engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*")) 62 return nil 63 } 64 65 func oddOrEven(n int) string { 66 if n%2 == 0 { 67 return "even" 68 } 69 return "odd" 70 } 71 72 func escape(str string) template.HTML { 73 /* #nosec G203 */ 74 return template.HTML(template.HTMLEscapeString(str)) 75 } 76 77 func noescape(str string) template.HTML { 78 /* #nosec G203 */ 79 return template.HTML(str) 80 } 81 82 func noescapeAttr(str string) template.HTMLAttr { 83 /* #nosec G203 */ 84 return template.HTMLAttr(str) 85 } 86 87 func timestamp(stamp string) string { 88 t, err := util.ParseISO8601(stamp) 89 if err != nil { 90 log.Errorf(nil, "error parsing timestamp %s: %s", stamp, err) 91 return badTimestamp 92 } 93 94 t = t.Local() 95 96 tYear, tMonth, tDay := t.Date() 97 now := time.Now() 98 currentYear, currentMonth, currentDay := now.Date() 99 100 switch { 101 case tYear == currentYear && tMonth == currentMonth && tDay == currentDay: 102 return "Today, " + t.Format(justTime) 103 case tYear == currentYear: 104 return t.Format(dateTime) 105 default: 106 return t.Format(dateYear) 107 } 108 } 109 110 func timestampPrecise(stamp string) string { 111 t, err := util.ParseISO8601(stamp) 112 if err != nil { 113 log.Errorf(nil, "error parsing timestamp %s: %s", stamp, err) 114 return badTimestamp 115 } 116 return t.Local().Format(dateYearTime) 117 } 118 119 func timestampVague(stamp string) string { 120 t, err := util.ParseISO8601(stamp) 121 if err != nil { 122 log.Errorf(nil, "error parsing timestamp %s: %s", stamp, err) 123 return badTimestamp 124 } 125 return t.Format(monthYear) 126 } 127 128 type iconWithLabel struct { 129 faIcon string 130 label string 131 } 132 133 func visibilityIcon(visibility apimodel.Visibility) template.HTML { 134 var icon iconWithLabel 135 136 switch visibility { 137 case apimodel.VisibilityPublic: 138 icon = iconWithLabel{"globe", "public"} 139 case apimodel.VisibilityUnlisted: 140 icon = iconWithLabel{"unlock", "unlisted"} 141 case apimodel.VisibilityPrivate: 142 icon = iconWithLabel{"lock", "private"} 143 case apimodel.VisibilityMutualsOnly: 144 icon = iconWithLabel{"handshake-o", "mutuals only"} 145 case apimodel.VisibilityDirect: 146 icon = iconWithLabel{"envelope", "direct"} 147 } 148 149 /* #nosec G203 */ 150 return template.HTML(fmt.Sprintf(`<i aria-label="Visibility: %v" class="fa fa-%v"></i>`, icon.label, icon.faIcon)) 151 } 152 153 // text is a template.HTML to affirm that the input of this function is already escaped 154 func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML { 155 out := text.Emojify(emojis, string(inputText)) 156 157 /* #nosec G203 */ 158 // (this is escaped above) 159 return template.HTML(out) 160 } 161 162 func acctInstance(acct string) string { 163 parts := strings.Split(acct, "@") 164 if len(parts) > 1 { 165 return "@" + parts[1] 166 } 167 168 return "" 169 } 170 171 func LoadTemplateFunctions(engine *gin.Engine) { 172 engine.SetFuncMap(template.FuncMap{ 173 "escape": escape, 174 "noescape": noescape, 175 "noescapeAttr": noescapeAttr, 176 "oddOrEven": oddOrEven, 177 "visibilityIcon": visibilityIcon, 178 "timestamp": timestamp, 179 "timestampVague": timestampVague, 180 "timestampPrecise": timestampPrecise, 181 "emojify": emojify, 182 "acctInstance": acctInstance, 183 }) 184 }