messageEvents.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. package chat
  2. import (
  3. "bytes"
  4. "regexp"
  5. "strings"
  6. "github.com/microcosm-cc/bluemonday"
  7. "github.com/owncast/owncast/models"
  8. "github.com/yuin/goldmark"
  9. emoji "github.com/yuin/goldmark-emoji"
  10. emojiAst "github.com/yuin/goldmark-emoji/ast"
  11. "github.com/yuin/goldmark/extension"
  12. "github.com/yuin/goldmark/renderer/html"
  13. "github.com/yuin/goldmark/util"
  14. "mvdan.cc/xurls"
  15. log "github.com/sirupsen/logrus"
  16. )
  17. // OutboundEvent represents an event that is sent out to all listeners of the chat server.
  18. type OutboundEvent interface {
  19. GetBroadcastPayload() models.EventPayload
  20. GetMessageType() models.EventType
  21. }
  22. // MessageEvent is an event that has a message body.
  23. type MessageEvent struct {
  24. OutboundEvent `json:"-"`
  25. Body string `json:"body"`
  26. RawBody string `json:"-"`
  27. }
  28. // SystemActionEvent is an event that represents an action that took place, not a chat message.
  29. type SystemActionEvent struct {
  30. models.Event
  31. MessageEvent
  32. }
  33. // RenderAndSanitizeMessageBody will turn markdown into HTML, sanitize raw user-supplied HTML and standardize
  34. // the message into something safe and renderable for clients.
  35. func (m *MessageEvent) RenderAndSanitizeMessageBody() {
  36. m.RawBody = m.Body
  37. // Set the new, sanitized and rendered message body
  38. m.Body = RenderAndSanitize(m.RawBody)
  39. }
  40. // Empty will return if this message's contents is empty.
  41. func (m *MessageEvent) Empty() bool {
  42. return m.Body == ""
  43. }
  44. // RenderBody will render markdown to html without any sanitization.
  45. func (m *MessageEvent) RenderBody() {
  46. m.RawBody = m.Body
  47. m.Body = RenderMarkdown(m.RawBody)
  48. }
  49. // RenderAndSanitize will turn markdown into HTML, sanitize raw user-supplied HTML and standardize
  50. // the message into something safe and renderable for clients.
  51. func RenderAndSanitize(raw string) string {
  52. rendered := RenderMarkdown(raw)
  53. safe := sanitize(rendered)
  54. // Set the new, sanitized and rendered message body
  55. return strings.TrimSpace(safe)
  56. }
  57. // RenderMarkdown will return HTML rendered from the string body of a chat message.
  58. func RenderMarkdown(raw string) string {
  59. // emojiMu.Lock()
  60. // defer emojiMu.Unlock()
  61. markdown := goldmark.New(
  62. goldmark.WithRendererOptions(
  63. html.WithUnsafe(),
  64. ),
  65. goldmark.WithExtensions(
  66. extension.NewLinkify(
  67. extension.WithLinkifyAllowedProtocols([][]byte{
  68. []byte("http:"),
  69. []byte("https:"),
  70. }),
  71. extension.WithLinkifyURLRegexp(
  72. xurls.Strict,
  73. ),
  74. ),
  75. emoji.New(
  76. emoji.WithEmojis(
  77. emojiDefs,
  78. ),
  79. emoji.WithRenderingMethod(emoji.Func),
  80. emoji.WithRendererFunc(func(w util.BufWriter, source []byte, n *emojiAst.Emoji, config *emoji.RendererConfig) {
  81. baseName := n.Value.ShortNames[0]
  82. _, _ = w.WriteString(emojiHTML[baseName])
  83. }),
  84. ),
  85. ),
  86. )
  87. trimmed := strings.TrimSpace(raw)
  88. var buf bytes.Buffer
  89. if err := markdown.Convert([]byte(trimmed), &buf); err != nil {
  90. log.Debugln(err)
  91. }
  92. return buf.String()
  93. }
  94. var (
  95. _sanitizeReSrcMatch = regexp.MustCompile(`(?i)^/img/emoji/[^\.%]*.[A-Z]*$`)
  96. _sanitizeReClassMatch = regexp.MustCompile(`(?i)^(emoji)[A-Z_]*?$`)
  97. _sanitizeNonEmptyMatch = regexp.MustCompile(`^.+$`)
  98. )
  99. func sanitize(raw string) string {
  100. p := bluemonday.StrictPolicy()
  101. // Require URLs to be parseable by net/url.Parse
  102. p.AllowStandardURLs()
  103. p.RequireParseableURLs(true)
  104. // Allow links
  105. p.AllowAttrs("href").OnElements("a")
  106. // Force all URLs to have "noreferrer" in their rel attribute.
  107. p.RequireNoReferrerOnLinks(true)
  108. // Links will get target="_blank" added to them.
  109. p.AddTargetBlankToFullyQualifiedLinks(true)
  110. // Allow breaks
  111. p.AllowElements("br")
  112. p.AllowElements("p")
  113. // Allow img tags from the the local emoji directory only
  114. p.AllowAttrs("src").Matching(_sanitizeReSrcMatch).OnElements("img")
  115. p.AllowAttrs("alt", "title").Matching(_sanitizeNonEmptyMatch).OnElements("img")
  116. p.AllowAttrs("class").Matching(_sanitizeReClassMatch).OnElements("img")
  117. // Allow bold
  118. p.AllowElements("strong")
  119. // Allow emphasis
  120. p.AllowElements("em")
  121. // Allow code blocks
  122. p.AllowElements("code")
  123. p.AllowElements("pre")
  124. return p.Sanitize(raw)
  125. }