Skip to content

Commit 814d5be

Browse files
committedNov 11, 2019
1 parent 956c468 commit 814d5be

File tree

10 files changed

+309
-57
lines changed

10 files changed

+309
-57
lines changed
 

‎python/server/auto_generated/qgsserverapiutils.sip.in

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ This method takes into account the ACL restrictions provided by QGIS Server Acce
115115
%End
116116

117117

118+
static const QVector<QgsMapLayer *> editableWfsLayers( const QgsProject *project, QgsServerRequest::Method &method );
119+
%Docstring
120+
Returns the list of layers accessible to the service in edit mode for a given ``project``.
121+
122+
:param method: represents the operation to be performed (POST, PUT, PATCH, DELETE)
123+
124+
This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.
125+
126+
.. note::
127+
128+
project must not be NULL
129+
130+
.. versionadded:: 3.12
131+
%End
132+
133+
118134
static QString sanitizedFieldValue( const QString &value );
119135
%Docstring
120136
Sanitizes the input ``value`` by removing URL encoding and checking for malicious content.

‎python/server/auto_generated/qgsserverrequest.sip.in

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class QgsServerRequest
1616
%TypeHeaderCode
1717
#include "qgsserverrequest.h"
1818
%End
19+
public:
20+
static const QMetaObject staticMetaObject;
21+
1922
public:
2023

2124
typedef QMap<QString, QString> Parameters;
@@ -27,7 +30,8 @@ class QgsServerRequest
2730
PutMethod,
2831
GetMethod,
2932
PostMethod,
30-
DeleteMethod
33+
DeleteMethod,
34+
PatchMethod
3135
};
3236

3337

@@ -56,6 +60,14 @@ Constructor
5660

5761
virtual ~QgsServerRequest();
5862

63+
static QString methodToString( const Method &method );
64+
%Docstring
65+
methodToString returns a string representation of an HTTP request ``method``
66+
67+
.. versionadded:: 3.12
68+
%End
69+
70+
5971
QUrl url() const;
6072
%Docstring
6173

‎src/server/qgsfcgiserverrequest.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ QgsFcgiServerRequest::QgsFcgiServerRequest()
106106
{
107107
method = HeadMethod;
108108
}
109+
else if ( strcmp( me, "PATCH" ) == 0 )
110+
{
111+
method = PatchMethod;
112+
}
109113
}
110114

111115
if ( method == PostMethod || method == PutMethod )

‎src/server/qgsrequesthandler.cpp

Lines changed: 57 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -189,70 +189,77 @@ void QgsRequestHandler::setupParameters()
189189

190190
void QgsRequestHandler::parseInput()
191191
{
192-
if ( mRequest.method() == QgsServerRequest::PostMethod )
192+
if ( mRequest.method() == QgsServerRequest::PostMethod ||
193+
mRequest.method() == QgsServerRequest::PutMethod ||
194+
mRequest.method() == QgsServerRequest::PatchMethod )
193195
{
194-
QString inputString( mRequest.data() );
195-
196-
QDomDocument doc;
197-
QString errorMsg;
198-
int line = -1;
199-
int column = -1;
200-
if ( !doc.setContent( inputString, true, &errorMsg, &line, &column ) )
196+
if ( mRequest.header( QStringLiteral( "Content-Type" ) ).contains( QStringLiteral( "json" ) ) )
201197
{
202-
// XXX Output error but continue processing request ?
203-
QgsMessageLog::logMessage( QStringLiteral( "Warning: error parsing post data as XML: at line %1, column %2: %3. Assuming urlencoded query string sent in the post body." )
204-
.arg( line ).arg( column ).arg( errorMsg ) );
205-
206-
// Process input string as a simple query text
207-
208-
typedef QPair<QString, QString> pair_t;
209-
QUrlQuery query( inputString );
210-
QList<pair_t> items = query.queryItems();
211-
for ( pair_t pair : items )
212-
{
213-
// QUrl::fromPercentEncoding doesn't replace '+' with space
214-
const QString key = QUrl::fromPercentEncoding( pair.first.replace( '+', ' ' ).toUtf8() );
215-
const QString value = QUrl::fromPercentEncoding( pair.second.replace( '+', ' ' ).toUtf8() );
216-
mRequest.setParameter( key.toUpper(), value );
217-
}
218198
setupParameters();
219199
}
220200
else
221201
{
222-
// we have an XML document
223-
224-
setupParameters();
225-
226-
QDomElement docElem = doc.documentElement();
227-
// the document element tag name is the request
228-
mRequest.setParameter( QStringLiteral( "REQUEST" ), docElem.tagName() );
229-
// loop through the attributes which are the parameters
230-
// excepting the attributes started by xmlns or xsi
231-
QDomNamedNodeMap map = docElem.attributes();
232-
for ( int i = 0 ; i < map.length() ; ++i )
202+
QString inputString( mRequest.data() );
203+
QDomDocument doc;
204+
QString errorMsg;
205+
int line = -1;
206+
int column = -1;
207+
if ( !doc.setContent( inputString, true, &errorMsg, &line, &column ) )
233208
{
234-
if ( map.item( i ).isNull() )
235-
continue;
236-
237-
const QDomNode attrNode = map.item( i );
238-
const QDomAttr attr = attrNode.toAttr();
239-
if ( attr.isNull() )
240-
continue;
241-
242-
const QString attrName = attr.name();
243-
if ( attrName.startsWith( "xmlns" ) || attrName.startsWith( "xsi:" ) )
244-
continue;
245-
246-
mRequest.setParameter( attrName.toUpper(), attr.value() );
209+
// XXX Output error but continue processing request ?
210+
QgsMessageLog::logMessage( QStringLiteral( "Warning: error parsing post data as XML: at line %1, column %2: %3. Assuming urlencoded query string sent in the post body." )
211+
.arg( line ).arg( column ).arg( errorMsg ) );
212+
213+
// Process input string as a simple query text
214+
215+
typedef QPair<QString, QString> pair_t;
216+
QUrlQuery query( inputString );
217+
QList<pair_t> items = query.queryItems();
218+
for ( pair_t pair : items )
219+
{
220+
// QUrl::fromPercentEncoding doesn't replace '+' with space
221+
const QString key = QUrl::fromPercentEncoding( pair.first.replace( '+', ' ' ).toUtf8() );
222+
const QString value = QUrl::fromPercentEncoding( pair.second.replace( '+', ' ' ).toUtf8() );
223+
mRequest.setParameter( key.toUpper(), value );
224+
}
225+
setupParameters();
226+
}
227+
else
228+
{
229+
// we have an XML document
230+
231+
setupParameters();
232+
233+
QDomElement docElem = doc.documentElement();
234+
// the document element tag name is the request
235+
mRequest.setParameter( QStringLiteral( "REQUEST" ), docElem.tagName() );
236+
// loop through the attributes which are the parameters
237+
// excepting the attributes started by xmlns or xsi
238+
QDomNamedNodeMap map = docElem.attributes();
239+
for ( int i = 0 ; i < map.length() ; ++i )
240+
{
241+
if ( map.item( i ).isNull() )
242+
continue;
243+
244+
const QDomNode attrNode = map.item( i );
245+
const QDomAttr attr = attrNode.toAttr();
246+
if ( attr.isNull() )
247+
continue;
248+
249+
const QString attrName = attr.name();
250+
if ( attrName.startsWith( "xmlns" ) || attrName.startsWith( "xsi:" ) )
251+
continue;
252+
253+
mRequest.setParameter( attrName.toUpper(), attr.value() );
254+
}
255+
mRequest.setParameter( QStringLiteral( "REQUEST_BODY" ), inputString );
247256
}
248-
mRequest.setParameter( QStringLiteral( "REQUEST_BODY" ), inputString );
249257
}
250258
}
251259
else
252260
{
253261
setupParameters();
254262
}
255-
256263
}
257264

258265
void QgsRequestHandler::setParameter( const QString &key, const QString &value )

‎src/server/qgsserverapiutils.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,60 @@ class SERVER_EXPORT QgsServerApiUtils
201201
return result;
202202
}
203203

204+
#endif
205+
206+
/**
207+
* Returns the list of layers accessible to the service in edit mode for a given \a project.
208+
* \param method represents the operation to be performed (POST, PUT, PATCH, DELETE)
209+
*
210+
* This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.
211+
*
212+
* \note project must not be NULL
213+
* \since QGIS 3.12
214+
*/
215+
static const QVector<QgsMapLayer *> editableWfsLayers( const QgsProject *project, QgsServerRequest::Method &method );
216+
217+
#ifndef SIP_RUN
218+
219+
/**
220+
* Returns the list of layers of type T accessible to the WFS service for a given \a project.
221+
*
222+
* Example:
223+
*
224+
* QVector<QgsVectorLayer*> vectorLayers = publishedLayers<QgsVectorLayer>();
225+
*
226+
* \note not available in Python bindings
227+
*/
228+
template <typename T>
229+
static const QVector<const T *> editableWfsLayers( const QgsServerApiContext &context, QgsServerRequest::Method &method )
230+
{
231+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
232+
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
233+
#endif
234+
const QgsProject *project = context.project();
235+
QVector<const T *> result;
236+
if ( project )
237+
{
238+
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
239+
const auto constLayers { project->layers<T *>() };
240+
for ( const auto &layer : constLayers )
241+
{
242+
if ( ! wfsLayerIds.contains( layer->id() ) )
243+
{
244+
continue;
245+
}
246+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
247+
if ( accessControl && !accessControl->layerInsertPermission( layer ) )
248+
{
249+
continue;
250+
}
251+
#endif
252+
result.push_back( layer );
253+
}
254+
}
255+
return result;
256+
}
257+
204258
#endif
205259

206260
/**

‎src/server/qgsserverexception.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,27 @@ class SERVER_EXPORT QgsServerApiBadRequestException: public QgsServerApiExceptio
251251
}
252252
};
253253

254+
255+
/**
256+
* \ingroup server
257+
* \class QgsServerApiPermissionDeniedException
258+
* \brief Forbidden (permission denied) 403
259+
*
260+
* Note that this exception is associated with a default return code 403 which may be
261+
* not appropriate in some situations.
262+
*
263+
* \since QGIS 3.12
264+
*/
265+
class SERVER_EXPORT QgsServerApiPermissionDeniedException: public QgsServerApiException
266+
{
267+
public:
268+
//! Construction
269+
QgsServerApiPermissionDeniedException( const QString &message, const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 403 )
270+
: QgsServerApiException( QStringLiteral( "Forbidden" ), message, mimeType, responseCode )
271+
{
272+
}
273+
};
274+
254275
/**
255276
* \ingroup server
256277
* \class QgsServerApiImproperlyConfiguredException

‎src/server/qgsserverrequest.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ QgsServerRequest::QgsServerRequest( const QUrl &url, Method method, const Header
3434
mParams.load( QUrlQuery( url ) );
3535
}
3636

37+
QString QgsServerRequest::methodToString( const QgsServerRequest::Method &method )
38+
{
39+
static QMetaEnum metaEnum = QMetaEnum::fromType<QgsServerRequest::Method>();
40+
return QString( metaEnum.valueToKey( method ) ).remove( QStringLiteral( "Method" ) ).toUpper( );
41+
}
42+
3743
QString QgsServerRequest::header( const QString &name ) const
3844
{
3945
return mHeaders.value( name );

‎src/server/qgsserverrequest.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
class SERVER_EXPORT QgsServerRequest
3939
{
40+
Q_GADGET
41+
4042
public:
4143

4244
typedef QMap<QString, QString> Parameters;
@@ -51,8 +53,10 @@ class SERVER_EXPORT QgsServerRequest
5153
PutMethod,
5254
GetMethod,
5355
PostMethod,
54-
DeleteMethod
56+
DeleteMethod,
57+
PatchMethod
5558
};
59+
Q_ENUM( Method )
5660

5761

5862
/**
@@ -81,6 +85,13 @@ class SERVER_EXPORT QgsServerRequest
8185
//! destructor
8286
virtual ~QgsServerRequest() = default;
8387

88+
/**
89+
* methodToString returns a string representation of an HTTP request \a method
90+
* \since QGIS 3.12
91+
*/
92+
static QString methodToString( const Method &method );
93+
94+
8495
/**
8596
* \returns the request url as seen by QGIS server
8697
*

‎src/server/services/wfs3/qgswfs3handlers.cpp

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "qgsserverapiutils.h"
2424
#include "qgsfeaturerequest.h"
2525
#include "qgsjsonutils.h"
26+
#include "qgsogrutils.h"
2627
#include "qgsvectorlayer.h"
2728
#include "qgsmessagelog.h"
2829
#include "qgsbufferserverrequest.h"
@@ -441,10 +442,12 @@ QgsWfs3CollectionsHandler::QgsWfs3CollectionsHandler()
441442
void QgsWfs3CollectionsHandler::handleRequest( const QgsServerApiContext &context ) const
442443
{
443444
json crss = json::array();
445+
444446
for ( const QString &crs : QgsServerApiUtils::publishedCrsList( context.project() ) )
445447
{
446448
crss.push_back( crs.toStdString() );
447449
}
450+
448451
json data
449452
{
450453
{
@@ -455,9 +458,9 @@ void QgsWfs3CollectionsHandler::handleRequest( const QgsServerApiContext &contex
455458
"crs", crss
456459
}
457460
};
461+
458462
if ( context.project() )
459463
{
460-
461464
const QgsProject *project = context.project();
462465
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
463466
for ( const QString &wfsLayerId : wfsLayerIds )
@@ -1309,9 +1312,78 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
13091312
};
13101313
write( data, context, htmlMetadata );
13111314
}
1315+
else if ( context.request()->method() == QgsServerRequest::Method::PostMethod )
1316+
{
1317+
// First: check permissions
1318+
const QStringList wfstInsertLayerIds = QgsServerProjectUtils::wfstInsertLayerIds( *context.project() );
1319+
if ( ! wfstInsertLayerIds.contains( mapLayer->id() ) || ! mapLayer->dataProvider()->capabilities().testFlag( QgsVectorDataProvider::Capability::AddFeatures ) )
1320+
{
1321+
throw QgsServerApiPermissionDeniedException( QStringLiteral( "Layer %1 is not editable" ).arg( mapLayer->name() ) );
1322+
}
1323+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
1324+
// get access controls
1325+
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
1326+
if ( accessControl && !accessControl->layerInsertPermission( mapLayer ) )
1327+
{
1328+
throw QgsServerApiPermissionDeniedException( QStringLiteral( "No ACL permissions to insert features on layer '%1'" ).arg( mapLayer->name() ) );
1329+
}
1330+
//scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
1331+
//there's LOTS of potential exit paths here, so we avoid having to restore the filters manually
1332+
std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() );
1333+
if ( accessControl )
1334+
{
1335+
QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, mapLayer, filterRestorer->originalFilters() );
1336+
}
1337+
#endif
1338+
try
1339+
{
1340+
// Parse
1341+
json postData = json::parse( context.request()->data() );
1342+
// Process data
1343+
const QgsFeatureList features = QgsOgrUtils::stringToFeatureList( context.request()->data(), mapLayer->fields(), QTextCodec::codecForName( "UTF-8" ) );
1344+
if ( features.isEmpty() )
1345+
{
1346+
throw QgsServerApiBadRequestException( QStringLiteral( "Posted body contains no feature" ) );
1347+
}
1348+
QgsFeature feat = features.first();
1349+
if ( ! feat.isValid() )
1350+
{
1351+
throw QgsServerApiInternalServerError( QStringLiteral( "Feature is not valid" ) );
1352+
}
1353+
// Make sure the first field (id) is null for shapefiles
1354+
if ( mapLayer->providerType() == QLatin1String( "ogr" ) && mapLayer->storageType() == QLatin1String( "ESRI Shapefile" ) )
1355+
{
1356+
feat.setAttribute( 0, QVariant() );
1357+
}
1358+
feat.setId( FID_NULL );
1359+
// TODO: handle CRS
1360+
QgsFeatureList featuresToAdd;
1361+
featuresToAdd.append( feat );
1362+
if ( ! mapLayer->dataProvider()->addFeatures( featuresToAdd ) )
1363+
{
1364+
throw QgsServerApiInternalServerError( QStringLiteral( "Error adding feature to collection" ) );
1365+
}
1366+
feat = featuresToAdd.first();
1367+
// Send response
1368+
context.response()->setStatusCode( 201 );
1369+
context.response()->setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "application/geo+json" ) );
1370+
QString url { context.request()->url().toString() };
1371+
if ( ! url.endsWith( '/' ) )
1372+
{
1373+
url.append( '/' );
1374+
}
1375+
context.response()->setHeader( QStringLiteral( "Location" ), url + QString::number( feat.id() ) );
1376+
context.response()->write( "\"string\"" );
1377+
}
1378+
catch ( json::exception &ex )
1379+
{
1380+
throw QgsServerApiBadRequestException( QStringLiteral( "JSON parse error: %1" ).arg( ex.what( ) ) );
1381+
}
1382+
}
13121383
else
13131384
{
1314-
throw QgsServerApiNotImplementedException( QStringLiteral( "Only GET method is implemented." ) );
1385+
throw QgsServerApiNotImplementedException( QStringLiteral( "%1 method is not implemented." )
1386+
.arg( QgsServerRequest::methodToString( context.request()->method() ) ) );
13151387
}
13161388

13171389
}
@@ -1344,11 +1416,11 @@ void QgsWfs3CollectionsFeatureHandler::handleRequest( const QgsServerApiContext
13441416
checkLayerIsAccessible( mapLayer, context );
13451417

13461418
const std::string title { mapLayer->title().isEmpty() ? mapLayer->name().toStdString() : mapLayer->title().toStdString() };
1419+
const QString featureId { match.captured( QStringLiteral( "featureId" ) ) };
13471420

1421+
// GET
13481422
if ( context.request()->method() == QgsServerRequest::Method::GetMethod )
13491423
{
1350-
const QString featureId { match.captured( QStringLiteral( "featureId" ) ) };
1351-
13521424
#ifdef HAVE_SERVER_PYTHON_PLUGINS
13531425
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
13541426
//scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
@@ -1393,7 +1465,8 @@ void QgsWfs3CollectionsFeatureHandler::handleRequest( const QgsServerApiContext
13931465
}
13941466
else
13951467
{
1396-
throw QgsServerApiNotImplementedException( QStringLiteral( "Only GET method is implemented." ) );
1468+
throw QgsServerApiNotImplementedException( QStringLiteral( "%1 method is not implemented." )
1469+
.arg( QgsServerRequest::methodToString( context.request()->method() ) ) );
13971470
}
13981471
}
13991472

‎tests/src/python/test_qgsserver_api.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,54 @@ def test_wfs3_static_handler(self):
518518
body = bytes(response.body()).decode('utf8')
519519
self.assertEqual(body, '[{"code":"API not found error","description":"Static file does_not_exists.css was not found"}]')
520520

521+
def test_wfs3_collection_items_post(self):
522+
"""Test WFS3 API items POST"""
523+
project = QgsProject()
524+
project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs')
525+
526+
# Invalid request
527+
data = b'not json!'
528+
request = QgsBufferServerRequest(
529+
'http://server.qgis.org/wfs3/collections/testlayer%20èé/items',
530+
QgsBufferServerRequest.PostMethod,
531+
{'Content-Type': 'application/geo+json'},
532+
data
533+
)
534+
response = QgsBufferServerResponse()
535+
self.server.handleRequest(request, response, project)
536+
self.assertEqual(response.statusCode(), 400)
537+
self.assertTrue('[{"code":"Bad request error","description":"JSON parse error' in bytes(response.body()).decode('utf8'))
538+
539+
# Valid request
540+
data = """{
541+
"geometry": {
542+
"coordinates": [
543+
8.111,
544+
44.55
545+
],
546+
"type": "Point"
547+
},
548+
"properties": {
549+
"id": 123,
550+
"name": "one + 123",
551+
"utf8nameè": "one èé + 123"
552+
},
553+
"type": "Feature"
554+
}""".encode('utf8')
555+
request = QgsBufferServerRequest(
556+
'http://server.qgis.org/wfs3/collections/testlayer%20èé/items',
557+
QgsBufferServerRequest.PostMethod,
558+
{'Content-Type': 'application/geo+json'},
559+
data
560+
)
561+
response = QgsBufferServerResponse()
562+
self.server.handleRequest(request, response, project)
563+
self.assertEqual(response.statusCode(), 201)
564+
self.assertEqual(response.body(), '"string"')
565+
# Get last fid
566+
fid = max(project.mapLayersByName('testlayer èé')[0].allFeatureIds())
567+
self.assertEqual(response.headers()['Location'], 'http://server.qgis.org/wfs3/collections/testlayer èé/items/%s' % fid)
568+
521569
def test_wfs3_field_filters(self):
522570
"""Test field filters"""
523571
project = QgsProject()

0 commit comments

Comments
 (0)
Please sign in to comment.