diff --git a/grails-app/services/au/org/ala/merit/EmailService.groovy b/grails-app/services/au/org/ala/merit/EmailService.groovy index 768902800..bbd84ed04 100644 --- a/grails-app/services/au/org/ala/merit/EmailService.groovy +++ b/grails-app/services/au/org/ala/merit/EmailService.groovy @@ -1,6 +1,7 @@ package au.org.ala.merit import au.org.ala.merit.config.EmailTemplate +import au.org.ala.merit.util.MarkdownUtils import groovy.util.logging.Slf4j @@ -17,7 +18,8 @@ class EmailService { def systemEmailAddress = grailsApplication.config.getProperty('fieldcapture.system.email.address') try { def subjectLine = settingService.getSettingText(mailSubjectTemplate, model) - def body = settingService.getSettingText(mailTemplate, model).markdownToHtml() + String bodyMarkdown = settingService.getSettingText(mailTemplate, model) + String body = MarkdownUtils.markdownToHtmlAndSanitise(bodyMarkdown) log.info("Sending email: ${subjectLine} to: ${recipient}, from: ${sender}, cc:${ccList}, body: ${body}") // This is to prevent spamming real users while testing. diff --git a/grails-app/taglib/au/org/ala/merit/FCTagLib.groovy b/grails-app/taglib/au/org/ala/merit/FCTagLib.groovy index 24f532e92..f6ba9bb3c 100644 --- a/grails-app/taglib/au/org/ala/merit/FCTagLib.groovy +++ b/grails-app/taglib/au/org/ala/merit/FCTagLib.groovy @@ -2,6 +2,7 @@ package au.org.ala.merit import au.org.ala.cas.util.AuthenticationCookieUtils import au.org.ala.merit.config.ProgramConfig +import au.org.ala.merit.util.MarkdownUtils import au.org.ala.web.AuthService import bootstrap.Attribute import grails.converters.JSON @@ -11,12 +12,6 @@ import groovy.xml.MarkupBuilder import org.apache.commons.lang.WordUtils import org.grails.web.json.JSONArray import org.grails.web.json.JSONObject -import org.owasp.html.HtmlChangeListener -import org.owasp.html.HtmlPolicyBuilder -import org.owasp.html.PolicyFactory -import org.owasp.html.Sanitizers -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer @Slf4j class FCTagLib { @@ -27,10 +22,6 @@ class FCTagLib { def userService def settingService AuthService authService - MetadataService metadataService - - /** Allow simple formatting, links and text within p and divs by default */ - def policy = (Sanitizers.FORMATTING & Sanitizers.LINKS & Sanitizers.BLOCKS) & new HtmlPolicyBuilder().allowTextIn("p", "div").toFactory() def textField = { attrs -> def outerClass = attrs.remove 'outerClass' @@ -1170,27 +1161,11 @@ class FCTagLib { def markdownToHtml = { Map attrs, body -> String text = attrs.text ?: body() - out << markdownToHtmlAndSanitise(text) + out << MarkdownUtils.markdownToHtmlAndSanitise(text) } private String markdownToHtmlAndSanitise(String text) { - Parser parser = Parser.builder().build() - org.commonmark.node.Node document = parser.parse(text) - HtmlRenderer renderer = HtmlRenderer.builder().build() - String html = renderer.render(document) - - internalSanitise(policy, html) - } - - private static String internalSanitise(PolicyFactory policyFactory, String input, String imageId = '', String metadataName = '') { - policyFactory.sanitize(input, new HtmlChangeListener() { - void discardedTag(Object context, String elementName) { - log.warn("Dropping element $elementName in $imageId.$metadataName") - } - void discardedAttributes(Object context, String tagName, String... attributeNames) { - log.warn("Dropping attributes $attributeNames from $tagName in $imageId.$metadataName") - } - }, null) + MarkdownUtils.markdownToHtmlAndSanitise(text) } private static String getScoreLabels(def scoreIds, ProgramConfig config, Boolean includeService) { diff --git a/src/main/groovy/au/org/ala/merit/util/MarkdownUtils.groovy b/src/main/groovy/au/org/ala/merit/util/MarkdownUtils.groovy new file mode 100644 index 000000000..948c670dc --- /dev/null +++ b/src/main/groovy/au/org/ala/merit/util/MarkdownUtils.groovy @@ -0,0 +1,39 @@ +package au.org.ala.merit.util + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.owasp.html.HtmlChangeListener +import org.owasp.html.HtmlPolicyBuilder +import org.owasp.html.PolicyFactory +import org.owasp.html.Sanitizers + +@CompileStatic +@Slf4j +class MarkdownUtils { + + /** Allow simple formatting, links and text within p and divs by default */ + static PolicyFactory policy = (Sanitizers.FORMATTING & Sanitizers.LINKS & Sanitizers.BLOCKS) & new HtmlPolicyBuilder().allowTextIn("p", "div").toFactory() + + static String markdownToHtmlAndSanitise(String text) { + Parser parser = Parser.builder().build() + org.commonmark.node.Node document = parser.parse(text) + HtmlRenderer renderer = HtmlRenderer.builder().build() + String html = renderer.render(document) + + internalSanitise(policy, html) + } + + private static String internalSanitise(PolicyFactory policyFactory, String input, String imageId = '', String metadataName = '') { + policyFactory.sanitize(input, new HtmlChangeListener() { + void discardedTag(Object context, String elementName) { + log.warn("Dropping element $elementName in $imageId.$metadataName") + } + void discardedAttributes(Object context, String tagName, String... attributeNames) { + log.warn("Dropping attributes $attributeNames from $tagName in $imageId.$metadataName") + } + }, null) + } + +} diff --git a/src/test/groovy/au/org/ala/merit/EmailServiceSpec.groovy b/src/test/groovy/au/org/ala/merit/EmailServiceSpec.groovy index d9de73d58..29b82c185 100644 --- a/src/test/groovy/au/org/ala/merit/EmailServiceSpec.groovy +++ b/src/test/groovy/au/org/ala/merit/EmailServiceSpec.groovy @@ -54,7 +54,6 @@ class EmailServiceSpec extends Specification implements AutowiredTest{ List usersAndRoles = [admin1, grantManager1, editor] EmailTemplate emailTemplate = EmailTemplate.DEFAULT_PLAN_SUBMITTED_EMAIL_TEMPLATE String body = "body" - body.metaClass.markdownToHtml = { "Body" } EmailParams email when: @@ -69,7 +68,7 @@ class EmailServiceSpec extends Specification implements AutowiredTest{ email.params.from == "merit@ala.org.au" email.params.replyTo == "merituser1@test.com" email.params.subject == "Subject" - email.params.html == "Body" + email.params.html == "

body

\n" } @@ -88,7 +87,6 @@ class EmailServiceSpec extends Specification implements AutowiredTest{ List usersAndRoles = [admin1, admin2, editor] EmailTemplate emailTemplate = EmailTemplate.DEFAULT_PLAN_APPROVED_EMAIL_TEMPLATE String body = "body" - body.metaClass.markdownToHtml = { "Body" } EmailParams email when: @@ -102,7 +100,7 @@ class EmailServiceSpec extends Specification implements AutowiredTest{ email.params.from == "merit@ala.org.au" email.params.replyTo == "merituser1@test.com" email.params.subject == "Subject" - email.params.html == "Body" + email.params.html == "

body

\n" } diff --git a/src/test/groovy/au/org/ala/merit/util/MarkdownUtilsSpec.groovy b/src/test/groovy/au/org/ala/merit/util/MarkdownUtilsSpec.groovy new file mode 100644 index 000000000..5f53efa3f --- /dev/null +++ b/src/test/groovy/au/org/ala/merit/util/MarkdownUtilsSpec.groovy @@ -0,0 +1,50 @@ +package au.org.ala.merit.util + +import spock.lang.Specification + +class MarkdownUtilsSpec extends Specification { + + def "markdownToHtmlAndSanitise should convert markdown to HTML and sanitize it"() { + given: + String markdown = "# Heading\n\nThis is a [link](http://example.com)." + + when: + String result = MarkdownUtils.markdownToHtmlAndSanitise(markdown) + + then: + result == "

Heading

\n

This is a link.

\n" + } + + def "markdownToHtmlAndSanitise should remove disallowed tags"() { + given: + String markdown = "" + + when: + String result = MarkdownUtils.markdownToHtmlAndSanitise(markdown) + + then: + result == "\n" + } + + def "markdownToHtmlAndSanitise should allow simple formatting"() { + given: + String markdown = "**bold** *italic*" + + when: + String result = MarkdownUtils.markdownToHtmlAndSanitise(markdown) + + then: + result == "

bold italic

\n" + } + + def "markdownToHtmlAndSanitise should allow text within p and div tags"() { + given: + String markdown = "

Paragraph

Division
" + + when: + String result = MarkdownUtils.markdownToHtmlAndSanitise(markdown) + + then: + result == "

Paragraph

Division
\n" + } +} \ No newline at end of file