diff options
| -rw-r--r-- | internal/router/template.go | 52 | ||||
| -rw-r--r-- | web/source/css/base.css | 8 | ||||
| -rw-r--r-- | web/template/status.tmpl | 4 | 
3 files changed, 62 insertions, 2 deletions
diff --git a/internal/router/template.go b/internal/router/template.go index ebd8629e8..a9d5726ea 100644 --- a/internal/router/template.go +++ b/internal/router/template.go @@ -19,7 +19,9 @@  package router  import ( +	"bytes"  	"fmt" +	"html"  	"html/template"  	"os"  	"path/filepath" @@ -28,6 +30,7 @@ import (  	"github.com/gin-gonic/gin"  	"github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/regexes"  )  // LoadTemplates loads html templates for use by the given engine @@ -57,6 +60,11 @@ func oddOrEven(n int) string {  	return "odd"  } +func escape(str string) template.HTML { +	/* #nosec G203 */ +	return template.HTML(template.HTMLEscapeString(str)) +} +  func noescape(str string) template.HTML {  	/* #nosec G203 */  	return template.HTML(str) @@ -97,12 +105,56 @@ func visibilityIcon(visibility model.Visibility) template.HTML {  	return template.HTML(fmt.Sprintf(`<i aria-label="Visibility: %v" class="fa fa-%v"></i>`, icon.label, icon.faIcon))  } +// replaces shortcodes in `text` with the emoji in `emojis` +// text is a template.HTML to affirm that the input of this function is already escaped +func emojify(emojis []model.Emoji, text template.HTML) template.HTML { +	emojisMap := make(map[string]model.Emoji, len(emojis)) + +	for _, emoji := range emojis { +		shortcode := ":" + emoji.Shortcode + ":" +		emojisMap[shortcode] = emoji +	} + +	out := regexes.ReplaceAllStringFunc( +		regexes.EmojiFinder, +		string(text), +		func(shortcode string, buf *bytes.Buffer) string { +			// Look for emoji according to this shortcode +			emoji, ok := emojisMap[shortcode] +			if !ok { +				return shortcode +			} + +			// Escape raw emoji content +			safeURL := html.EscapeString(emoji.URL) +			safeCode := html.EscapeString(emoji.Shortcode) + +			// Write HTML emoji repr to buffer +			buf.WriteString(`<img src="`) +			buf.WriteString(safeURL) +			buf.WriteString(`" title=":`) +			buf.WriteString(safeCode) +			buf.WriteString(`:" alt=":`) +			buf.WriteString(safeCode) +			buf.WriteString(`:" class="emoji"/>`) + +			return buf.String() +		}, +	) + +	/* #nosec G203 */ +	// (this is escaped above) +	return template.HTML(out) +} +  func LoadTemplateFunctions(engine *gin.Engine) {  	engine.SetFuncMap(template.FuncMap{ +		"escape":         escape,  		"noescape":       noescape,  		"oddOrEven":      oddOrEven,  		"visibilityIcon": visibilityIcon,  		"timestamp":      timestamp,  		"timestampShort": timestampShort, +		"emojify":        emojify,  	})  } diff --git a/web/source/css/base.css b/web/source/css/base.css index ba9fef606..3cdf19fe8 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -323,3 +323,11 @@ footer {  		grid-template-columns: 1fr;  	}  } + +.emoji { +	width: 2.5ex; +	height: 2.5ex; +	margin: -0.5ex 0 0; +	object-fit: contain; +	vertical-align: middle; +}
\ No newline at end of file diff --git a/web/template/status.tmpl b/web/template/status.tmpl index decad4764..56d98d89b 100644 --- a/web/template/status.tmpl +++ b/web/template/status.tmpl @@ -6,12 +6,12 @@  		{{if .SpoilerText}}  		<input class="spoiler" id="hideSpoiler-{{.ID}}" type="checkbox" style="display: none" aria-hidden="true" checked="true" />  		<div class="spoiler"> -			<span class="spoiler-text">{{.SpoilerText}}</span> +			<span class="spoiler-text">{{emojify .Emojis (escape .SpoilerText)}}</span>  			<label class="button spoiler-label" for="hideSpoiler-{{.ID}}" tabindex="0">Toggle visibility</label>  		</div>  		{{end}}  		<div class="content"> -			{{.Content |noescape}} +			{{emojify .Emojis (noescape .Content)}}  		</div>  	</div>  	{{with .MediaAttachments}}  | 
