diff --git a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala index 91ca6e06db..d4eaf634cd 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -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._ @@ -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)) + ) + + } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala index f354638f69..4936fc00ac 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala @@ -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) + } } /** @@ -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. * @@ -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. * @@ -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. * @@ -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. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala index abcef8929c..93a1a0ef47 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala @@ -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) @@ -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 => @@ -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, @@ -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) @@ -663,7 +663,7 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde children = children.map(_.sorted), position = position, hasRootNode = hasRootNode - ) + ).unescape } for { @@ -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 diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/NewListsRouteADMFeature.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/NewListsRouteADMFeature.scala index 2767bf8ff4..ee69811032 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/NewListsRouteADMFeature.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/NewListsRouteADMFeature.scala @@ -142,7 +142,7 @@ 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() @@ -150,7 +150,7 @@ class NewListsRouteADMFeature(routeData: KnoraRouteData) } 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() diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala index 14267cc8ae..f458d1cdf7 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala @@ -128,7 +128,7 @@ class OldListsRouteADMFeature(routeData: KnoraRouteData) ) } yield ListCreateRequestADM( - createRootNode = apiRequest, + createRootNode = apiRequest.escape, featureFactoryConfig = featureFactoryConfig, requestingUser = requestingUser, apiRequestID = UUID.randomUUID() @@ -266,7 +266,7 @@ class OldListsRouteADMFeature(routeData: KnoraRouteData) ) } yield ListChildNodeCreateRequestADM( - createChildNodeRequest = apiRequest, + createChildNodeRequest = apiRequest.escape, featureFactoryConfig = featureFactoryConfig, requestingUser = requestingUser, apiRequestID = UUID.randomUUID() diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala index 6bd6cb05fc..19d3a340d8 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala @@ -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._ @@ -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"), @@ -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 @@ -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, @@ -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) @@ -239,7 +280,7 @@ 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 @@ -247,7 +288,7 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with 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) }