11 changes: 11 additions & 0 deletions src/server/qgsserverogcapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include <QDir>
#include <QDebug>
#include <QtGlobal>

#include "qgsserverogcapi.h"
#include "qgsserverogcapihandler.h"
Expand Down Expand Up @@ -68,7 +69,17 @@ void QgsServerOgcApi::registerHandler( QgsServerOgcApiHandler *handler )

QUrl QgsServerOgcApi::sanitizeUrl( const QUrl &url )
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
// Since QT 13 NormalizePathSegments does not collapse double slashes
QUrl u { url.adjusted( QUrl::StripTrailingSlash | QUrl::NormalizePathSegments ) };
while ( u.path().contains( QLatin1String( "//" ) ) )
{
u.setPath( u.path().replace( QLatin1String( "//" ), QChar( '/' ) ) );
}
return u;
#else
return url.adjusted( QUrl::StripTrailingSlash | QUrl::NormalizePathSegments );
#endif
}

void QgsServerOgcApi::executeRequest( const QgsServerApiContext &context ) const
Expand Down
2 changes: 1 addition & 1 deletion src/server/qgsserverogcapihandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ void QgsServerOgcApiHandler::htmlDump( const json &data, const QgsServerApiConte
QFileInfo fi{ url.path() };
auto suffix { fi.suffix() };
auto fName { fi.filePath()};
if ( suffix.length() != 0 )
if ( !suffix.isEmpty() )
{
fName.chop( suffix.length() + 1 );
}
Expand Down
6 changes: 6 additions & 0 deletions src/server/qgsserverrequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ QgsServerRequest::QgsServerRequest( const QUrl &url, Method method, const Header
mParams.load( QUrlQuery( url ) );
}

QString QgsServerRequest::methodToString( const QgsServerRequest::Method &method )
{
static QMetaEnum metaEnum = QMetaEnum::fromType<QgsServerRequest::Method>();
return QString( metaEnum.valueToKey( method ) ).remove( QStringLiteral( "Method" ) ).toUpper( );
}

QString QgsServerRequest::header( const QString &name ) const
{
return mHeaders.value( name );
Expand Down
13 changes: 12 additions & 1 deletion src/server/qgsserverrequest.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@

class SERVER_EXPORT QgsServerRequest
{
Q_GADGET

public:

typedef QMap<QString, QString> Parameters;
Expand All @@ -51,8 +53,10 @@ class SERVER_EXPORT QgsServerRequest
PutMethod,
GetMethod,
PostMethod,
DeleteMethod
DeleteMethod,
PatchMethod
};
Q_ENUM( Method )


/**
Expand Down Expand Up @@ -81,6 +85,13 @@ class SERVER_EXPORT QgsServerRequest
//! destructor
virtual ~QgsServerRequest() = default;

/**
* Returns a string representation of an HTTP request \a method.
* \since QGIS 3.12
*/
static QString methodToString( const Method &method );


/**
* \returns the request url as seen by QGIS server
*
Expand Down
1,003 changes: 782 additions & 221 deletions src/server/services/wfs3/qgswfs3handlers.cpp

Large diffs are not rendered by default.

31 changes: 16 additions & 15 deletions src/server/services/wfs3/qgswfs3handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@

class QgsFeatureRequest;
class QgsServerOgcApi;
class QgsFeature;

/**
* The QgsWfs3AbstractItemsHandler class provides some
* functionality which is common to the handlers that
* return items.
* return or process items.
*/
class QgsWfs3AbstractItemsHandler: public QgsServerOgcApiHandler
{
Expand All @@ -41,10 +42,10 @@ class QgsWfs3AbstractItemsHandler: public QgsServerOgcApiHandler
* \param context the server api context
* \throws QgsServerApiNotFoundException if the layer is NOT published
*/
void checkLayerIsAccessible( const QgsVectorLayer *layer, const QgsServerApiContext &context ) const;
void checkLayerIsAccessible( QgsVectorLayer *layer, const QgsServerApiContext &context ) const;

/**
* Creates a filtered QgsFeatureRequest containing only fields published for WMS and plugin filters applied.
* Creates a filtered QgsFeatureRequest containing only fields published for WFS and plugin filters applied.
* \param layer the vector layer
* \param context the server api context
* \param subsetAttributes optional list of field names requested by the client, if empty all published attributes will be added to the request
Expand All @@ -53,10 +54,10 @@ class QgsWfs3AbstractItemsHandler: public QgsServerOgcApiHandler
QgsFeatureRequest filteredRequest( const QgsVectorLayer *layer, const QgsServerApiContext &context, const QStringList &subsetAttributes = QStringList() ) const;

/**
* Returns a filtered list of fields containing only fields published for WMS and plugin filters applied.
* @param layer the vector layer
* @param context the server api context
* @return QgsFields list with filters applied
* Returns a filtered list of fields containing only fields published for WFS and plugin filters applied.
* \param layer the vector layer
* \param context the server api context
* \return QgsFields list with filters applied
*/
QgsFields publishedFields( const QgsVectorLayer *layer, const QgsServerApiContext &context ) const;

Expand All @@ -75,9 +76,9 @@ class QgsWfs3APIHandler: public QgsWfs3AbstractItemsHandler
void handleRequest( const QgsServerApiContext &context ) const override;
QRegularExpression path() const override { return QRegularExpression( R"re(/api)re" ); }
std::string operationId() const override { return "getApiDescription"; }
std::string summary() const override { return "The API definition"; }
std::string summary() const override { return "The API description"; }
std::string description() const override { return "The formal documentation of this API according to the OpenAPI specification, version 3.0. I.e., this document."; }
std::string linkTitle() const override { return "API definition"; }
std::string linkTitle() const override { return "API description"; }
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::service_desc; }
json schema( const QgsServerApiContext &context ) const override;
Expand Down Expand Up @@ -149,11 +150,11 @@ class QgsWfs3ConformanceHandler: public QgsServerOgcApiHandler
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re(/conformance)re" ); }
std::string operationId() const override { return "getRequirementClasses"; }
std::string summary() const override { return "Information about standards that this API conforms to"; }
std::string summary() const override { return "Information about standards that this API conforms to."; }
std::string description() const override
{
return "List all requirements classes specified in a standard (e.g., WFS 3.0 "
"Part 1: Core) that the server conforms to";
"Part 1: Core) that the server conforms to.";
}
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
std::string linkTitle() const override { return "WFS 3.0 conformance classes"; }
Expand Down Expand Up @@ -204,7 +205,7 @@ class QgsWfs3DescribeCollectionHandler: public QgsWfs3AbstractItemsHandler

QRegularExpression path() const override { return QRegularExpression( R"re(/collections/(?<collectionId>[^/]+?)(\.json|\.html)?$)re" ); }
std::string operationId() const override { return "describeCollection"; }
std::string summary() const override { return "Describe the feature collection"; }
std::string summary() const override { return "Describe the feature collection with ID {collectionId}."; }
std::string description() const override { return "Metadata about a feature collection."; }
std::string linkTitle() const override { return "Feature collection"; }
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
Expand All @@ -223,7 +224,7 @@ class QgsWfs3CollectionsItemsHandler: public QgsWfs3AbstractItemsHandler
void handleRequest( const QgsServerApiContext &context ) const override;
QRegularExpression path() const override { return QRegularExpression( R"re(/collections/(?<collectionId>[^/]+)/items(\.geojson|\.json|\.html)?$)re" ); }
std::string operationId() const override { return "getFeatures"; }
std::string summary() const override { return "Retrieve features of feature collection collectionId"; }
std::string summary() const override { return "Retrieve features of feature collection {collectionId}."; }
std::string description() const override
{
return "Every feature in a dataset belongs to a collection. A dataset may "
Expand Down Expand Up @@ -252,8 +253,8 @@ class QgsWfs3CollectionsFeatureHandler: public QgsWfs3AbstractItemsHandler
void handleRequest( const QgsServerApiContext &context ) const override;
QRegularExpression path() const override { return QRegularExpression( R"re(/collections/(?<collectionId>[^/]+)/items/(?<featureId>[^/]+?)(\.json|\.geojson|\.html)?$)re" ); }
std::string operationId() const override { return "getFeature"; }
std::string description() const override { return "Retrieve a feature; use content negotiation or specify a file extension to request HTML (.html or GeoJSON (.json)"; }
std::string summary() const override { return "Retrieve a single feature"; }
std::string description() const override { return "Retrieve a feature with ID {featureId} from the collection with ID {collectionId}; use content negotiation or specify a file extension to request HTML (.html or GeoJSON (.json)."; }
std::string summary() const override { return "Retrieve a single feature with ID {featureId} from the collection with ID {collectionId}."; }
std::string linkTitle() const override { return "Retrieve a feature"; }
QStringList tags() const override { return { QStringLiteral( "Features" ) }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::data; }
Expand Down
418 changes: 404 additions & 14 deletions tests/src/python/test_qgsserver_api.py

Large diffs are not rendered by default.

708 changes: 696 additions & 12 deletions tests/testdata/qgis_server/api/test_wfs3_api_project.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ Content-Type: application/json
{
"href": "http://server.qgis.org/wfs3/collections/points/items.json",
"rel": "items",
"title": "points",
"type": "application/json"
"title": "points items as GEOJSON",
"type": "application/geo+json"
},
{
"href": "http://server.qgis.org/wfs3/collections/points/items.html",
"rel": "items",
"title": "points",
"title": "points items as HTML",
"type": "text/html"
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,23 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous">
<script src="/wfs3/static/jsonFormatter.min.js" crossorigin="anonymous"></script>

<!-- template for the WFS3 API links list, to be included in HEAD -->

<link rel="alternate" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9.json" title="Feature collection as JSON" type="application/json">

<link rel="self" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9.html" title="Feature collection as HTML" type="text/html">

<link rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json" title="A test vector layer èé items as GEOJSON" type="application/geo+json">

<link rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.html" title="A test vector layer èé items as HTML" type="text/html">

<link rel="describedBy" href="http://server.qgis.org/?request=DescribeFeatureType&typenames=testlayer%20èé&service=WFS&version=2.0" title="Schema for A test vector layer èé" type="application/xml">



<title>A test vector layer èé</title>
</head>
<body>
Expand Down Expand Up @@ -86,6 +100,29 @@ <h3>Extent</h3>
<dt>44.901482526001836</dt>
</dl>

<h3>Links</h3>
<ul>


<li><a rel="alternate" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9.json">Feature collection as JSON</a></li>





<li><a rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json">A test vector layer èé items as GEOJSON</a></li>



<li><a rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.html">A test vector layer èé items as HTML</a></li>



<li><a rel="describedBy" href="http://server.qgis.org/?request=DescribeFeatureType&typenames=testlayer%20èé&service=WFS&version=2.0">Schema for A test vector layer èé</a></li>


</ul>

</div>

<div class="col-md-6">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ Content-Type: application/json
{
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json",
"rel": "items",
"title": "A test vector layer èé",
"type": "application/json"
"title": "A test vector layer èé items as GEOJSON",
"type": "application/geo+json"
},
{
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.html",
"rel": "items",
"title": "A test vector layer èé",
"title": "A test vector layer èé items as HTML",
"type": "text/html"
},
{
Expand Down
10 changes: 9 additions & 1 deletion tests/testdata/qgis_server/api/test_wfs3_collections_empty.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous">
<script src="/wfs3/static/jsonFormatter.min.js" crossorigin="anonymous"></script>

<!-- template for the WFS3 API links list, to be included in HEAD -->

<link rel="alternate" href="http://server.qgis.org/wfs3/collections.json" title="Feature collections as JSON" type="application/json">

<link rel="self" href="http://server.qgis.org/wfs3/collections.html" title="Feature collections as HTML" type="text/html">



<title>Feature collections</title>
</head>
<body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous">
<script src="/wfs3/static/jsonFormatter.min.js" crossorigin="anonymous"></script>

<!-- template for the WFS3 API links list, to be included in HEAD -->

<link rel="alternate" href="http://server.qgis.org/wfs3/collections.json" title="Feature collections as JSON" type="application/json">

<link rel="self" href="http://server.qgis.org/wfs3/collections.html" title="Feature collections as HTML" type="text/html">



<title>Feature collections</title>
</head>
<body>
Expand Down
20 changes: 17 additions & 3 deletions tests/testdata/qgis_server/api/test_wfs3_landing_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,23 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous">
<script src="/wfs3/static/jsonFormatter.min.js" crossorigin="anonymous"></script>

<!-- template for the WFS3 API links list, to be included in HEAD -->

<link rel="alternate" href="http://server.qgis.org/wfs3.json" title="Landing page as JSON" type="application/json">

<link rel="self" href="http://server.qgis.org/wfs3.html" title="Landing page as HTML" type="text/html">

<link rel="data" href="http://server.qgis.org/wfs3/collections" title="Feature collections" type="application/json">

<link rel="conformance" href="http://server.qgis.org/wfs3/conformance" title="Conformance classes" type="application/json">

<link rel="service-desc" href="http://server.qgis.org/wfs3/api" title="API description" type="application/openapi+json;version=3.0">



<title>Landing page</title>
</head>
<body>
Expand Down Expand Up @@ -63,13 +77,13 @@ <h2>Available services</h2>



<li><a rel="conformance" href="http://server.qgis.org/wfs3/conformance">WFS 3.0 conformance classes</a></li>
<li><a rel="conformance" href="http://server.qgis.org/wfs3/conformance">Conformance classes</a></li>





<li><a rel="service-desc" href="http://server.qgis.org/wfs3/api">API definition</a></li>
<li><a rel="service-desc" href="http://server.qgis.org/wfs3/api">API description</a></li>



Expand Down
4 changes: 2 additions & 2 deletions tests/testdata/qgis_server/api/test_wfs3_landing_page.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ Content-Type: application/json
{
"href": "http://server.qgis.org/wfs3/conformance",
"rel": "conformance",
"title": "WFS 3.0 conformance classes",
"title": "Conformance classes",
"type": "application/json"
},
{
"href": "http://server.qgis.org/wfs3/api",
"rel": "service-desc",
"title": "API definition",
"title": "API description",
"type": "application/openapi+json;version=3.0"
}
],
Expand Down
Binary file not shown.
2,200 changes: 2,200 additions & 0 deletions tests/testdata/qgis_server/test_project_api_editing.qgs

Large diffs are not rendered by default.