diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5d6b9c24e..79712a1474 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -315,9 +315,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - # publish only on release + # publish on merge to main publish: - name: Publish (on release only) + name: Publish (on merge to main) needs: [ api-unit-tests, api-e2e-tests, @@ -326,7 +326,7 @@ jobs: docs-build-test ] runs-on: ubuntu-latest - if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags') + if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v1 with: diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index e7dc8e12ea..04ef7e54a4 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -1336,10 +1336,10 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt throw BadRequestException(s"Resource $gravsearchTemplateIri is not a Gravsearch template (text file expected)") } - gravsearchFileValueContent: TextFileValueContentV2 = resource.values.get(OntologyConstants.KnoraBase.HasTextFileValue.toSmartIri) match { + (fileValueIri: IRI, gravsearchFileValueContent: TextFileValueContentV2) = resource.values.get(OntologyConstants.KnoraBase.HasTextFileValue.toSmartIri) match { case Some(values: Seq[ReadValueV2]) if values.size == 1 => values.head match { case value: ReadValueV2 => value.valueContent match { - case textRepr: TextFileValueContentV2 => textRepr + case textRepr: TextFileValueContentV2 => (value.valueIri, textRepr) case _ => throw InconsistentTriplestoreDataException(s"Resource $gravsearchTemplateIri is supposed to have exactly one value of type ${OntologyConstants.KnoraBase.TextFileValue}") } } @@ -1347,9 +1347,9 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt case None => throw InconsistentTriplestoreDataException(s"Resource $gravsearchTemplateIri has no property ${OntologyConstants.KnoraBase.HasTextFileValue}") } - // check if `xsltFileValue` represents an XSL transformation - _ = if (!(gravsearchFileValueContent.fileValue.internalMimeType == "text/plain" && gravsearchFileValueContent.fileValue.internalFilename.endsWith(".txt"))) { - throw BadRequestException(s"Resource $gravsearchTemplateIri does not have a file value referring to a Gravsearch template") + // check if gravsearchFileValueContent represents a text file + _ = if (gravsearchFileValueContent.fileValue.internalMimeType != "text/plain") { + throw BadRequestException(s"Expected $fileValueIri to be a text file referring to a Gravsearch template, but it has MIME type ${gravsearchFileValueContent.fileValue.internalMimeType}") } gravsearchUrl: String = s"${settings.internalSipiBaseUrl}/${resource.projectADM.shortcode}/${gravsearchFileValueContent.fileValue.internalFilename}/file" diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index 421172463c..d28e8a7d3d 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -173,10 +173,10 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon throw BadRequestException(s"Resource $xslTransformationIri is not a ${OntologyConstants.KnoraBase.XSLTransformation}") } - xsltFileValueContent: TextFileValueContentV2 = resource.values.get(OntologyConstants.KnoraBase.HasTextFileValue.toSmartIri) match { + (fileValueIri: IRI, xsltFileValueContent: TextFileValueContentV2) = resource.values.get(OntologyConstants.KnoraBase.HasTextFileValue.toSmartIri) match { case Some(values: Seq[ReadValueV2]) if values.size == 1 => values.head match { case value: ReadValueV2 => value.valueContent match { - case textRepr: TextFileValueContentV2 => textRepr + case textRepr: TextFileValueContentV2 => (value.valueIri, textRepr) case _ => throw InconsistentTriplestoreDataException(s"${OntologyConstants.KnoraBase.XSLTransformation} $xslTransformationIri is supposed to have exactly one value of type ${OntologyConstants.KnoraBase.TextFileValue}") } } @@ -184,9 +184,9 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon case None => throw InconsistentTriplestoreDataException(s"${OntologyConstants.KnoraBase.XSLTransformation} has no property ${OntologyConstants.KnoraBase.HasTextFileValue}") } - // check if xsltFileValue represents an XSL transformation - _ = if (!(xmlMimeTypes.contains(xsltFileValueContent.fileValue.internalMimeType) && xsltFileValueContent.fileValue.internalFilename.endsWith(".xsl"))) { - throw BadRequestException(s"$xslTransformationIri does not have a file value referring to an XSL transformation") + // check if xsltFileValueContent represents an XSL transformation + _ = if (!xmlMimeTypes.contains(xsltFileValueContent.fileValue.internalMimeType)) { + throw BadRequestException(s"Expected $fileValueIri to be an XML file referring to an XSL transformation, but it has MIME type ${xsltFileValueContent.fileValue.internalMimeType}") } xsltUrl: String = s"${settings.internalSipiBaseUrl}/${resource.projectADM.shortcode}/${xsltFileValueContent.fileValue.internalFilename}/file" diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala index b5945bf2b1..20648bef8f 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala @@ -27,7 +27,7 @@ import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser import org.knora.webapi.messages.v2.responder.searchmessages._ -import org.knora.webapi.messages.{SmartIri, StringFormatter} +import org.knora.webapi.messages.{OntologyConstants, SmartIri, StringFormatter} import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV2} import scala.concurrent.Future @@ -152,13 +152,17 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def fullTextSearchCount(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "search" / "count" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space + private def fullTextSearchCount(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "search" / "count" / Segment) { searchStr => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => - val searchString = stringFormatter.toSparqlEncodedString(searchval, throw BadRequestException(s"Invalid search string: '$searchval'")) + if (searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname)) { + throw BadRequestException("It looks like you are submitting a Gravsearch request to a full-text search route") + } - if (searchString.length < settings.searchValueMinLength) { - throw BadRequestException(s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$searchString' given of length ${searchString.length}.") + val escapedSearchStr = stringFormatter.toSparqlEncodedString(searchStr, throw BadRequestException(s"Invalid search string: '$searchStr'")) + + if (escapedSearchStr.length < settings.searchValueMinLength) { + throw BadRequestException(s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}.") } val params: Map[String, String] = requestContext.request.uri.query().toMap @@ -172,7 +176,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[FullTextSearchCountRequestV2] = for { requestingUser <- getUserADM(requestContext) } yield FullTextSearchCountRequestV2( - searchValue = searchString, + searchValue = escapedSearchStr, limitToProject = limitToProject, limitToResourceClass = limitToResourceClass, limitToStandoffClass = limitToStandoffClass, @@ -192,13 +196,17 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def fullTextSearch(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "search" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space + private def fullTextSearch(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "search" / Segment) { searchStr => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => { - val searchString = stringFormatter.toSparqlEncodedString(searchval, throw BadRequestException(s"Invalid search string: '$searchval'")) + if (searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname)) { + throw BadRequestException("It looks like you are submitting a Gravsearch request to a full-text search route") + } - if (searchString.length < settings.searchValueMinLength) { - throw BadRequestException(s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$searchString' given of length ${searchString.length}.") + val escapedSearchStr = stringFormatter.toSparqlEncodedString(searchStr, throw BadRequestException(s"Invalid search string: '$searchStr'")) + + if (escapedSearchStr.length < settings.searchValueMinLength) { + throw BadRequestException(s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}.") } val params: Map[String, String] = requestContext.request.uri.query().toMap @@ -217,7 +225,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[FulltextSearchRequestV2] = for { requestingUser <- getUserADM(requestContext) } yield FulltextSearchRequestV2( - searchValue = searchString, + searchValue = escapedSearchStr, offset = offset, limitToProject = limitToProject, limitToResourceClass = limitToResourceClass, diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index 1c0106b5e7..104bb96a9c 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -198,6 +198,16 @@ class SearchRouteV2R2RSpec extends R2RSpec { } } + "not accept a fulltext query containing http://api.knora.org" in { + val invalidSearchString: String = URLEncoder.encode("PREFIX knora-api: ", "UTF-8") + + Get(s"/v2/search/$invalidSearchString") ~> searchPath ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.BAD_REQUEST, responseStr) + assert(responseStr.contains("It looks like you are submitting a Gravsearch request to a full-text search route")) + } + } + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Queries without type inference