Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(lists): Escape special characters in comment, label, and name of a list node (DSP-1529) #1846

Merged
merged 5 commits into from Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -39,7 +39,12 @@ import org.knora.webapi._
import org.knora.webapi.exceptions._
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM
import org.knora.webapi.messages.store.triplestoremessages.{SparqlAskRequest, SparqlAskResponse}
import org.knora.webapi.messages.store.triplestoremessages.{
SparqlAskRequest,
SparqlAskResponse,
StringLiteralSequenceV2,
StringLiteralV2
}
import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1
import org.knora.webapi.messages.v2.responder.KnoraContentV2
import org.knora.webapi.messages.v2.responder.standoffmessages._
Expand Down Expand Up @@ -3248,4 +3253,12 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
throw BadRequestException(s"Internal ontology <$iri> cannot be served")
}
}

def unescapeStringLiteralSeq(stringLiteralSeq: StringLiteralSequenceV2): StringLiteralSequenceV2 = {
StringLiteralSequenceV2(
stringLiterals = stringLiteralSeq.stringLiterals.map(stringLiteral =>
StringLiteralV2(value = fromSparqlEncodedString(stringLiteral.value), language = stringLiteral.language))
)

}
}
Expand Up @@ -125,6 +125,30 @@ case class CreateNodeApiRequestADM(id: Option[IRI] = None,
}

def toJsValue: JsValue = createListNodeApiRequestADMFormat.write(this)

/**
* Escapes special characters within strings
*
*/
def escape: CreateNodeApiRequestADM = {
val escapedLabels: Seq[StringLiteralV2] = labels.map { label =>
val escapedLabel =
stringFormatter.toSparqlEncodedString(label.value, throw BadRequestException(s"Invalid label: ${label.value}"))
StringLiteralV2(value = escapedLabel, language = label.language)
}
val escapedComments = comments.map { comment =>
val escapedComment =
stringFormatter.toSparqlEncodedString(comment.value,
throw BadRequestException(s"Invalid comment: ${comment.value}"))
StringLiteralV2(value = escapedComment, language = comment.language)
}
val escapedName: Option[String] = name match {
case None => None
case Some(value: String) =>
Some(stringFormatter.toSparqlEncodedString(value, throw BadRequestException(s"Invalid string: $value")))
}
copy(labels = escapedLabels, comments = escapedComments, name = escapedName)
}
}

/**
Expand Down Expand Up @@ -634,6 +658,23 @@ case class ListRootNodeInfoADM(id: IRI,
)
}

/**
* unescapes the special characters in labels, comments, and name for comparison in tests.
*
*/
def unescape: ListRootNodeInfoADM = {
val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

val unescapedLabels = stringFormatter.unescapeStringLiteralSeq(labels)

val unescapedComments = stringFormatter.unescapeStringLiteralSeq(comments)
val unescapedName: Option[String] = name match {
case None => None
case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value))
}
copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments)
}

/**
* Gets the label in the user's preferred language.
*
Expand Down Expand Up @@ -682,6 +723,23 @@ case class ListChildNodeInfoADM(id: IRI,
)
}

/**
* unescapes the special characters in labels, comments, and name for comparison in tests.
*
*/
def unescape: ListChildNodeInfoADM = {
val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

val unescapedLabels = stringFormatter.unescapeStringLiteralSeq(labels)

val unescapedComments = stringFormatter.unescapeStringLiteralSeq(comments)
val unescapedName: Option[String] = name match {
case None => None
case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value))
}
copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments)
}

/**
* Gets the label in the user's preferred language.
*
Expand Down Expand Up @@ -790,6 +848,22 @@ case class ListRootNodeADM(id: IRI,
)
}

/**
* unescapes the special characters in labels, comments, and name for comparison in tests.
*
*/
def unescape: ListRootNodeADM = {
val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

val unescapedLabels = stringFormatter.unescapeStringLiteralSeq(labels)
val unescapedComments = stringFormatter.unescapeStringLiteralSeq(comments)
val unescapedName: Option[String] = name match {
case None => None
case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value))
}
copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments)
}

/**
* Gets the label in the user's preferred language.
*
Expand Down Expand Up @@ -850,6 +924,23 @@ case class ListChildNodeADM(id: IRI,
)
}

/**
* unescapes the special characters in labels, comments, and name for comparison in tests.
*
*/
def unescape: ListChildNodeADM = {
val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

val unescapedLabels = stringFormatter.unescapeStringLiteralSeq(labels)
val unescapedComments = stringFormatter.unescapeStringLiteralSeq(comments)

val unescapedName: Option[String] = name match {
case None => None
case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value))
}
copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments)
}

/**
* Gets the label in the user's preferred language.
*
Expand Down
Expand Up @@ -154,7 +154,7 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde
name = name,
labels = StringLiteralSequenceV2(labels.toVector.sortBy(_.language)),
comments = StringLiteralSequenceV2(comments.toVector.sortBy(_.language))
)
).unescape
}

// _ = log.debug("listsGetAdminRequest - items: {}", items)
Expand Down Expand Up @@ -200,7 +200,7 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde

rootNodeInfo = maybeRootNodeInfo match {
case Some(info: ListRootNodeInfoADM) => info.asInstanceOf[ListRootNodeInfoADM]
case Some(info: ListChildNodeInfoADM) =>
case Some(_: ListChildNodeInfoADM) =>
throw InconsistentRepositoryDataException(
"A child node info was found, although we are expecting a root node info. Please report this as a possible bug.")
case Some(_) | None =>
Expand Down Expand Up @@ -393,7 +393,7 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde
.map(_.head.asInstanceOf[StringLiteralV2].value),
labels = StringLiteralSequenceV2(labels.toVector.sortBy(_.language)),
comments = StringLiteralSequenceV2(comments.toVector.sortBy(_.language))
)
).unescape
} else {
ListChildNodeInfoADM(
id = nodeIri.toString,
Expand All @@ -408,7 +408,7 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde
hasRootNode = hasRootNodeOption.getOrElse(
throw InconsistentRepositoryDataException(
s"Required hasRootNode property missing for list node $nodeIri."))
)
).unescape
}
}
Some(nodeInfo)
Expand Down Expand Up @@ -663,7 +663,7 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde
children = children.map(_.sorted),
position = position,
hasRootNode = hasRootNode
)
).unescape
}

for {
Expand Down Expand Up @@ -891,8 +891,10 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde
/* verify that the list node name is unique for the project */
projectUniqueNodeName <- listNodeNameIsProjectUnique(createNodeRequest.projectIri, createNodeRequest.name)
_ = if (!projectUniqueNodeName) {
val escapedName = createNodeRequest.name.get
val unescapedName = stringFormatter.fromSparqlEncodedString(escapedName)
throw BadRequestException(
s"The node name ${createNodeRequest.name.get} is already used by a list inside the project ${createNodeRequest.projectIri}.")
s"The node name ${unescapedName} is already used by a list inside the project ${createNodeRequest.projectIri}.")
}

// calculate the data named graph
Expand Down
Expand Up @@ -142,15 +142,15 @@ class NewListsRouteADMFeature(routeData: KnoraRouteData)
createRequest = if (apiRequest.parentNodeIri.isEmpty) {
// No, create a new list with given information of its root node.
ListCreateRequestADM(
createRootNode = apiRequest,
createRootNode = apiRequest.escape,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser,
apiRequestID = UUID.randomUUID()
)
} else {
// Yes, create a new child and attach it to the parent node.
ListChildNodeCreateRequestADM(
createChildNodeRequest = apiRequest,
createChildNodeRequest = apiRequest.escape,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser,
apiRequestID = UUID.randomUUID()
Expand Down
Expand Up @@ -128,7 +128,7 @@ class OldListsRouteADMFeature(routeData: KnoraRouteData)
)
} yield
ListCreateRequestADM(
createRootNode = apiRequest,
createRootNode = apiRequest.escape,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser,
apiRequestID = UUID.randomUUID()
Expand Down Expand Up @@ -266,7 +266,7 @@ class OldListsRouteADMFeature(routeData: KnoraRouteData)
)
} yield
ListChildNodeCreateRequestADM(
createChildNodeRequest = apiRequest,
createChildNodeRequest = apiRequest.escape,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser,
apiRequestID = UUID.randomUUID()
Expand Down
Expand Up @@ -26,6 +26,7 @@ import akka.testkit._
import com.typesafe.config.{Config, ConfigFactory}
import org.knora.webapi._
import org.knora.webapi.exceptions.{BadRequestException, DuplicateValueException, UpdateNotPerformedException}
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.admin.responder.listsmessages._
import org.knora.webapi.messages.store.triplestoremessages.{RdfDataObject, StringLiteralV2}
import org.knora.webapi.sharedtestdata.SharedTestDataV1._
Expand All @@ -51,6 +52,7 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with

// The default timeout for receiving reply messages from actors.
implicit private val timeout = 5.seconds
private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

override lazy val rdfDataObjects = List(
RdfDataObject(path = "test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"),
Expand Down Expand Up @@ -179,7 +181,7 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with
name = Some("neuelistename"),
labels = Seq(StringLiteralV2(value = "Neue Liste", language = Some("de"))),
comments = Seq.empty[StringLiteralV2]
),
).escape,
featureFactoryConfig = defaultFeatureFactoryConfig,
requestingUser = SharedTestDataADM.imagesUser01,
apiRequestID = UUID.randomUUID
Expand All @@ -206,8 +208,46 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with
newListIri.set(listInfo.id)
}

"create a list with special characters in its labels" in {
val labelWithSpecialCharacter = "Neue \\\"Liste\\\""
val commentWithSpecialCharacter = "Neue \\\"Kommentar\\\""
val nameWithSpecialCharacter = "a new \\\"name\\\""
responderManager ! ListCreateRequestADM(
createRootNode = CreateNodeApiRequestADM(
projectIri = IMAGES_PROJECT_IRI,
name = Some(nameWithSpecialCharacter),
labels = Seq(StringLiteralV2(value = labelWithSpecialCharacter, language = Some("de"))),
comments = Seq(StringLiteralV2(value = commentWithSpecialCharacter, language = Some("de"))),
).escape,
featureFactoryConfig = defaultFeatureFactoryConfig,
requestingUser = SharedTestDataADM.imagesUser01,
apiRequestID = UUID.randomUUID
)

val received: ListGetResponseADM = expectMsgType[ListGetResponseADM](timeout)

val listInfo = received.list.listinfo
listInfo.projectIri should be(IMAGES_PROJECT_IRI)

listInfo.name should be(Some(stringFormatter.fromSparqlEncodedString(nameWithSpecialCharacter)))

val labels: Seq[StringLiteralV2] = listInfo.labels.stringLiterals
labels.size should be(1)
val givenLabel = labels.head
givenLabel.value shouldEqual stringFormatter.fromSparqlEncodedString(labelWithSpecialCharacter)
givenLabel.language shouldEqual Some("de")

val comments = received.list.listinfo.comments.stringLiterals
val givenComment = comments.head
givenComment.language shouldEqual Some("de")
givenComment.value shouldEqual stringFormatter.fromSparqlEncodedString(commentWithSpecialCharacter)

val children = received.list.children
children.size should be(0)
}

"update basic list information" in {
responderManager ! NodeInfoChangeRequestADM(
val changeNodeInfoRequest = NodeInfoChangeRequestADM(
listIri = newListIri.get,
changeNodeRequest = ChangeNodeInfoApiRequestADM(
listIri = newListIri.get,
Expand All @@ -216,18 +256,19 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with
labels = Some(
Seq(
StringLiteralV2(value = "Neue geänderte Liste", language = Some("de")),
StringLiteralV2(value = "Changed list", language = Some("en"))
StringLiteralV2(value = "Changed List", language = Some("en"))
)),
comments = Some(
Seq(
StringLiteralV2(value = "Neuer Kommentar", language = Some("de")),
StringLiteralV2(value = "New comment", language = Some("en"))
StringLiteralV2(value = "New Comment", language = Some("en"))
))
),
featureFactoryConfig = defaultFeatureFactoryConfig,
requestingUser = SharedTestDataADM.imagesUser01,
apiRequestID = UUID.randomUUID
)
responderManager ! changeNodeInfoRequest

val received: RootNodeInfoGetResponseADM = expectMsgType[RootNodeInfoGetResponseADM](timeout)

Expand All @@ -239,15 +280,15 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with
labels.sorted should be(
Seq(
StringLiteralV2(value = "Neue geänderte Liste", language = Some("de")),
StringLiteralV2(value = "Changed list", language = Some("en"))
StringLiteralV2(value = "Changed List", language = Some("en"))
).sorted)

val comments = listInfo.comments.stringLiterals
comments.size should be(2)
comments.sorted should be(
Seq(
StringLiteralV2(value = "Neuer Kommentar", language = Some("de")),
StringLiteralV2(value = "New comment", language = Some("en"))
StringLiteralV2(value = "New Comment", language = Some("en"))
).sorted)
}

Expand Down