diff --git a/app/com/ysoft/html/HtmlWithText.scala b/app/com/ysoft/html/HtmlWithText.scala new file mode 100644 index 0000000..ebc499a --- /dev/null +++ b/app/com/ysoft/html/HtmlWithText.scala @@ -0,0 +1,27 @@ +package com.ysoft.html + +import play.twirl.api.{Html, HtmlFormat} + +object HtmlWithText{ + + def justText(s: String): HtmlWithText = HtmlWithText(html = HtmlFormat.empty, text = s) + def justHtml(h: Html): HtmlWithText = HtmlWithText(html = h, text = "") + def justHtml(h: String): HtmlWithText = justHtml(Html(h)) + def plainText(s: String): HtmlWithText = HtmlWithText(text = s, html = HtmlFormat.escape(s)) + + implicit class RichHtmlWithTextTraversable(val traversable: Traversable[HtmlWithText]) extends AnyVal { + def mkHtmlWithText(textSep: String, htmlSep: Html): HtmlWithText = HtmlWithText( + text = traversable.map(_.text).mkString("\n"), + html = Html(traversable.map(_.html).mkString(htmlSep.toString())) + ) + def mkHtmlWithText(sep: HtmlWithText): HtmlWithText = mkHtmlWithText(sep.text, sep.html) + + } + +} +case class HtmlWithText(html: Html, text: String){ + def +(other: HtmlWithText) = HtmlWithText( + html = Html(this.html.toString + other.html.toString), + text = this.text + other.text + ) +} diff --git a/app/services/EmailExportService.scala b/app/services/EmailExportService.scala index cc4941d..bef94b2 100644 --- a/app/services/EmailExportService.scala +++ b/app/services/EmailExportService.scala @@ -3,15 +3,17 @@ package services import java.util.NoSuchElementException import com.mohiva.play.silhouette.api.LoginInfo +import com.ysoft.html.HtmlWithText +import com.ysoft.html.HtmlWithText._ import com.ysoft.odc.{Absolutizer, SetDiff} import controllers._ import models.Change.Direction import models.{Change, EmailMessageId} import play.api.libs.mailer.{Email, MailerClient} +import play.twirl.api.{Html, HtmlFormat} import scala.concurrent.{ExecutionContext, Future} - object EmailExportType extends Enumeration { val Vulnerabilities = Value("vulnerabilities") val Digest = Value("digest") @@ -81,36 +83,45 @@ class EmailExportService(from: String, nobodyInterestedContact: String, val expo groups = changes.groupBy(_.direction).withDefaultValue(Seq()) } yield { val changesMarks = Map(Direction.Added -> "❢", Direction.Removed -> "☑") - def vulnerabilityText(change: Change, vulnerability: Vulnerability) = ( - s"#### ${changesMarks(change.direction)} ${vulnerability.name}${vulnerability.cvssScore.fold("")(sev => s" (CVSS severity: $sev)")}" - +"\n"+vulnerability.description - +"\nmore info: "+absolutizer.absolutize(routes.Statistics.vulnerability(vulnerability.name, None)) + def heading(level: Int)(s: String) = HtmlWithText( + html = Html(""+HtmlFormat.escape(s)+""), + text = ("#"*level) + s + "\n" ) - def vulnChanges(changes: Seq[Change]) = + def moreInfo(link: String) = HtmlWithText( + text = "more info: "+link, + html = Html("more info") + ) + def vulnerabilityText(change: Change, vulnerability: Vulnerability): HtmlWithText = ( + heading(4)(s"${changesMarks(change.direction)} ${vulnerability.name}${vulnerability.cvssScore.fold("")(sev => s" (CVSS severity: $sev)")}") + + justHtml("

") + plainText(vulnerability.description) + justHtml("
") + justText("\n") + + moreInfo(absolutizer.absolutize(routes.Statistics.vulnerability(vulnerability.name, None))) + justHtml("

") + ) + def vulnChanges(changes: Seq[Change]): HtmlWithText = changes.map(c => c -> vulns(c.vulnerabilityName)) .sortBy{case (change, vuln) => (vuln.cvssScore.map(-_), vuln.name)} .map((vulnerabilityText _).tupled) - .mkString("\n\n") - def vulnerableProjects(projectIdToChanges: Map[String, Seq[Change]]) = + .mkHtmlWithText(justText("\n\n")) + def vulnerableProjects(projectIdToChanges: Map[String, Seq[Change]]): HtmlWithText = projectIdToChanges.toIndexedSeq.map{case (project, ch) => (projects.parseUnfriendlyNameGracefully(project), ch)} .sortBy{case (ri, _) => friendlyProjectNameString(ri).toLowerCase} - .map{case (project, changes) => "### "+friendlyProjectNameString(project)+"\n"+vulnChanges(changes)} - .mkString("\n\n") - def section(title: String, direction: Direction) = { + .map{case (project, changes) => heading(3)(friendlyProjectNameString(project))+vulnChanges(changes)} + .mkHtmlWithText(justText("\n\n")) + def section(title: String, direction: Direction): Option[HtmlWithText] = { groups(direction) match { case Seq() => None - case list => Some("## "+title + "\n\n" + vulnerableProjects(list.groupBy(_.projectName))) + case list => Some(heading(2)(title) + justText("\n") + vulnerableProjects(list.groupBy(_.projectName))) } } + val body = Seq( + section("Projects newly affected by a vulnerability", Direction.Added), + section("Projects no longer affected by a vulnerability", Direction.Removed) + ).flatten.mkHtmlWithText(justText("\n\n")) Email( subject = s"New changes in vulnerabilities (${changes.size}: +${groups(Direction.Added).size} -${groups(Direction.Removed).size})", to = Seq(getEmail(subscriber)), from = from, - bodyText = Some(Seq( - section("Projects newly affected by a vulnerability", Direction.Added), - section("Projects no longer affected by a vulnerability", Direction.Removed) - ).flatten.mkString("\n\n")) - //bodyHtml = TODO + bodyText = Some(body.text), + bodyHtml = Some(body.html.toString) ) } }