Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions core/src/main/scala/org/apache/spark/ui/UIUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,16 @@ private[spark] object UIUtils extends Logging {
* Note: In terms of security, only anchor tags with root relative links are supported. So any
* attempts to embed links outside Spark UI, or other tags like <script> will cause in the whole
* description to be treated as plain text.
*
* @param desc the original job or stage description string, which may contain html tags.
* @param basePathUri with which to prepend the relative links; this is used when plainText is
* false.
* @param plainText whether to keep only plain text (i.e. remove html tags) from the original
* description string.
* @return the HTML rendering of the job or stage description, which will be a Text when plainText
* is true, and an Elem otherwise.
*/
def makeDescription(desc: String, basePathUri: String): NodeSeq = {
def makeDescription(desc: String, basePathUri: String, plainText: Boolean = false): NodeSeq = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment in the java doc to explain what plainText means

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewor14 sure; updated with the new commit, thanks!

import scala.language.postfixOps

// If the description can be parsed as HTML and has only relative links, then render
Expand Down Expand Up @@ -445,22 +453,37 @@ private[spark] object UIUtils extends Logging {
"Links in job descriptions must be root-relative:\n" + allLinks.mkString("\n\t"))
}

// Prepend the relative links with basePathUri
val rule = new RewriteRule() {
override def transform(n: Node): Seq[Node] = {
n match {
case e: Elem if e \ "@href" nonEmpty =>
val relativePath = e.attribute("href").get.toString
val fullUri = s"${basePathUri.stripSuffix("/")}/${relativePath.stripPrefix("/")}"
e % Attribute(null, "href", fullUri, Null)
case _ => n
val rule =
if (plainText) {
// Remove all tags, retaining only their texts
new RewriteRule() {
override def transform(n: Node): Seq[Node] = {
n match {
case e: Elem if e.child isEmpty => Text(e.text)
case e: Elem if e.child nonEmpty => Text(e.child.flatMap(transform).text)
case _ => n
}
}
}
}
else {
// Prepend the relative links with basePathUri
new RewriteRule() {
override def transform(n: Node): Seq[Node] = {
n match {
case e: Elem if e \ "@href" nonEmpty =>
val relativePath = e.attribute("href").get.toString
val fullUri = s"${basePathUri.stripSuffix("/")}/${relativePath.stripPrefix("/")}"
e % Attribute(null, "href", fullUri, Null)
case _ => n
}
}
}
}
}
new RuleTransformer(rule).transform(xml)
} catch {
case NonFatal(e) =>
<span class="description-input">{desc}</span>
if (plainText) Text(desc) else <span class="description-input">{desc}</span>
}
}

Expand Down
10 changes: 8 additions & 2 deletions core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
val jobId = jobUIData.jobId
val status = jobUIData.status
val (jobName, jobDescription) = getLastStageNameAndDescription(jobUIData)
val displayJobDescription = if (jobDescription.isEmpty) jobName else jobDescription
val displayJobDescription =
if (jobDescription.isEmpty) {
jobName
} else {
UIUtils.makeDescription(jobDescription, "", plainText = true).text
}
val submissionTime = jobUIData.submissionTime.get
val completionTimeOpt = jobUIData.completionTime
val completionTime = completionTimeOpt.getOrElse(System.currentTimeMillis())
Expand Down Expand Up @@ -225,7 +230,8 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
val formattedDuration = duration.map(d => UIUtils.formatDuration(d)).getOrElse("Unknown")
val formattedSubmissionTime = job.submissionTime.map(UIUtils.formatDate).getOrElse("Unknown")
val basePathUri = UIUtils.prependBaseUri(parent.basePath)
val jobDescription = UIUtils.makeDescription(lastStageDescription, basePathUri)
val jobDescription =
UIUtils.makeDescription(lastStageDescription, basePathUri, plainText = false)

val detailUrl = "%s/jobs/job?id=%s".format(basePathUri, job.jobId)
<tr id={"job-" + job.jobId}>
Expand Down
74 changes: 65 additions & 9 deletions core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,95 @@

package org.apache.spark.ui

import scala.xml.Elem
import scala.xml.{Node, Text}

import org.apache.spark.SparkFunSuite

class UIUtilsSuite extends SparkFunSuite {
import UIUtils._

test("makeDescription") {
test("makeDescription(plainText = false)") {
verify(
"""test <a href="/link"> text </a>""",
<span class="description-input">test <a href="/link"> text </a></span>,
"Correctly formatted text with only anchors and relative links should generate HTML"
"Correctly formatted text with only anchors and relative links should generate HTML",
plainText = false
)

verify(
"""test <a href="/link" text </a>""",
<span class="description-input">{"""test <a href="/link" text </a>"""}</span>,
"Badly formatted text should make the description be treated as a streaming instead of HTML"
"Badly formatted text should make the description be treated as a string instead of HTML",
plainText = false
)

verify(
"""test <a href="link"> text </a>""",
<span class="description-input">{"""test <a href="link"> text </a>"""}</span>,
"Non-relative links should make the description be treated as a string instead of HTML"
"Non-relative links should make the description be treated as a string instead of HTML",
plainText = false
)

verify(
"""test<a><img></img></a>""",
<span class="description-input">{"""test<a><img></img></a>"""}</span>,
"Non-anchor elements should make the description be treated as a string instead of HTML"
"Non-anchor elements should make the description be treated as a string instead of HTML",
plainText = false
)

verify(
"""test <a href="/link"> text </a>""",
<span class="description-input">test <a href="base/link"> text </a></span>,
baseUrl = "base",
errorMsg = "Base URL should be prepended to html links"
errorMsg = "Base URL should be prepended to html links",
plainText = false
)
}

test("makeDescription(plainText = true)") {
verify(
"""test <a href="/link"> text </a>""",
Text("test text "),
"Correctly formatted text with only anchors and relative links should generate a string " +
"without any html tags",
plainText = true
)

verify(
"""test <a href="/link"> text1 </a> <a href="/link"> text2 </a>""",
Text("test text1 text2 "),
"Correctly formatted text with multiple anchors and relative links should generate a " +
"string without any html tags",
plainText = true
)

verify(
"""test <a href="/link"><span> text </span></a>""",
Text("test text "),
"Correctly formatted text with nested anchors and relative links and/or spans should " +
"generate a string without any html tags",
plainText = true
)

verify(
"""test <a href="/link" text </a>""",
Text("""test <a href="/link" text </a>"""),
"Badly formatted text should make the description be as the same as the original text",
plainText = true
)

verify(
"""test <a href="link"> text </a>""",
Text("""test <a href="link"> text </a>"""),
"Non-relative links should make the description be as the same as the original text",
plainText = true
)

verify(
"""test<a><img></img></a>""",
Text("""test<a><img></img></a>"""),
"Non-anchor elements should make the description be as the same as the original text",
plainText = true
)
}

Expand Down Expand Up @@ -82,8 +134,12 @@ class UIUtilsSuite extends SparkFunSuite {
}

private def verify(
desc: String, expected: Elem, errorMsg: String = "", baseUrl: String = ""): Unit = {
val generated = makeDescription(desc, baseUrl)
desc: String,
expected: Node,
errorMsg: String = "",
baseUrl: String = "",
plainText: Boolean): Unit = {
val generated = makeDescription(desc, baseUrl, plainText)
assert(generated.sameElements(expected),
s"\n$errorMsg\n\nExpected:\n$expected\nGenerated:\n$generated")
}
Expand Down