diff --git a/docs/03-apis/api-admin/projects.md b/docs/03-apis/api-admin/projects.md index 11113bc24a..bc4a00b076 100644 --- a/docs/03-apis/api-admin/projects.md +++ b/docs/03-apis/api-admin/projects.md @@ -61,7 +61,7 @@ License along with DSP. If not, see . - Required information: - shortcode (unique, 4-digits) - shortname (unique; it should be in the form of a - [xsd:NCNAME](https://www.w3.org/TR/xmlschema11-2/#NCName)) + [xsd:NCNAME](https://www.w3.org/TR/xmlschema11-2/#NCName) and it should be URL safe.) - description (collection of descriptions as strings with language tag.) - keywords (collection of keywords) - status (true, if project is active. false, if project is inactive) diff --git a/docs/03-apis/api-v2/knora-iris.md b/docs/03-apis/api-v2/knora-iris.md index bdeccc5c56..d1d0598e24 100644 --- a/docs/03-apis/api-v2/knora-iris.md +++ b/docs/03-apis/api-v2/knora-iris.md @@ -68,7 +68,7 @@ http://www.knora.org/ontology/0001/example ``` An ontology name must be a valid XML -[NCName](https://www.w3.org/TR/xml-names/#NT-NCName). +[NCName](https://www.w3.org/TR/xml-names/#NT-NCName) and must be URL safe. The following names are reserved for built-in internal DSP ontologies: - `knora-base` 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 57ab202182..f36ec52cd9 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -852,6 +852,8 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No // RFC 4648. private val Base64UrlPattern = "[A-Za-z0-9_-]+" + private val Base64UrlPatternRegex: Regex = ("^" + Base64UrlPattern + "$").r + // Calculates check digits for resource IDs in ARK URLs. private val base64UrlCheckDigit = new Base64UrlCheckDigit @@ -2176,11 +2178,18 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No * @return the same ontology name. */ def validateProjectSpecificOntologyName(ontologyName: String, errorFun: => Nothing): String = { + // Check that ontology name matched NCName regex pattern ontologyName match { case NCNameRegex(_*) => () case _ => errorFun } + // Check that ontology name is URL safe + ontologyName match { + case Base64UrlPatternRegex(_*) => () + case _ => errorFun + } + val lowerCaseOntologyName = ontologyName.toLowerCase lowerCaseOntologyName match { @@ -2481,8 +2490,14 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No * project shortname. * @return the same string. */ - def validateAndEscapeProjectShortname(value: String, errorFun: => Nothing): String = { - NCNameRegex.findFirstIn(value) match { + def validateAndEscapeProjectShortname(shortname: String, errorFun: => Nothing): String = { + // Check that shortname matches NCName pattern + val ncNameMatch = NCNameRegex.findFirstIn(shortname) match { + case Some(value) => value + case None => errorFun + } + // Check that shortname is URL safe + Base64UrlPatternRegex.findFirstIn(ncNameMatch) match { case Some(shortname) => toSparqlEncodedString(shortname, errorFun) case None => errorFun } diff --git a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala index 6fd2f90199..0395efea60 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala @@ -108,6 +108,40 @@ class ProjectsMessagesADMSpec extends CoreSpec(ProjectsMessagesADMSpec.config) { ) assert(caught.getMessage === "Project shortcode must be supplied.") } + + "return 'BadRequestException' if project 'shortname' is not NCName valid" in { + val invalidShortName = "abd%2" + val caught = intercept[BadRequestException]( + CreateProjectApiRequestADM( + shortname = invalidShortName, + shortcode = "1114", + longname = Some("project longname"), + description = Seq(StringLiteralV2(value = "project description", language = Some("en"))), + keywords = Seq("keywords"), + logo = Some("/fu/bar/baz.jpg"), + status = true, + selfjoin = false + ).validateAndEscape + ) + assert(caught.getMessage === s"The supplied short name: '$invalidShortName' is not valid.") + } + + "return 'BadRequestException' if project 'shortname' is not URL safe" in { + val invalidShortName = "äbd2" + val caught = intercept[BadRequestException]( + CreateProjectApiRequestADM( + shortname = invalidShortName, + shortcode = "1114", + longname = Some("project longname"), + description = Seq(StringLiteralV2(value = "project description", language = Some("en"))), + keywords = Seq("keywords"), + logo = Some("/fu/bar/baz.jpg"), + status = true, + selfjoin = false + ).validateAndEscape + ) + assert(caught.getMessage === s"The supplied short name: '$invalidShortName' is not valid.") + } } "The ChangeProjectApiRequestADM case class" should { diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala index dc3371fb96..8f06c9de66 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala @@ -245,6 +245,23 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { fooLastModDate = newFooLastModDate } + "not create an ontology if the given name matches NCName pattern but is not URL safe" in { + responderManager ! CreateOntologyRequestV2( + ontologyName = "bär", + projectIri = imagesProjectIri, + label = "The bär ontology", + comment = Some("some comment"), + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = imagesUser + ) + + expectMsgPF(timeout) { + case msg: akka.actor.Status.Failure => + msg.cause.isInstanceOf[BadRequestException] should ===(true) + } + } + "create an empty ontology called 'bar' with a comment" in { responderManager ! CreateOntologyRequestV2( ontologyName = "bar",