1 /***************************************************************************
2                               qgswfsgetfeature.cpp
3                               -------------------------
4   begin                : December 20 , 2016
5   copyright            : (C) 2007 by Marco Hugentobler  (original code)
6                          (C) 2012 by René-Luc D'Hont    (original code)
7                          (C) 2014 by Alessandro Pasotti (original code)
8                          (C) 2017 by David Marteau
9   email                : marco dot hugentobler at karto dot baug dot ethz dot ch
10                          a dot pasotti at itopen dot it
11                          david dot marteau at 3liz dot com
12  ***************************************************************************/
13 
14 /***************************************************************************
15  *                                                                         *
16  *   This program is free software; you can redistribute it and/or modify  *
17  *   it under the terms of the GNU General Public License as published by  *
18  *   the Free Software Foundation; either version 2 of the License, or     *
19  *   (at your option) any later version.                                   *
20  *                                                                         *
21  ***************************************************************************/
22 #include "qgswfsutils.h"
23 #include "qgsserverprojectutils.h"
24 #include "qgsserverfeatureid.h"
25 #include "qgsfields.h"
26 #include "qgsdatetimefieldformatter.h"
27 #include "qgsexpression.h"
28 #include "qgsgeometry.h"
29 #include "qgsmaplayer.h"
30 #include "qgsfeatureiterator.h"
31 #include "qgscoordinatereferencesystem.h"
32 #include "qgsvectorlayer.h"
33 #include "qgsfilterrestorer.h"
34 #include "qgsproject.h"
35 #include "qgsogcutils.h"
36 #include "qgsjsonutils.h"
37 #include "qgsexpressioncontextutils.h"
38 #include "qgswkbtypes.h"
39 
40 #include "qgswfsgetfeature.h"
41 
42 namespace QgsWfs
43 {
44 
45   namespace
46   {
47     struct createFeatureParams
48     {
49       int precision;
50 
51       const QgsCoordinateReferenceSystem &crs;
52 
53       const QgsAttributeList &attributeIndexes;
54 
55       const QString &typeName;
56 
57       bool withGeom;
58 
59       const QString &geometryName;
60 
61       const QgsCoordinateReferenceSystem &outputCrs;
62 
63       bool forceGeomToMulti;
64 
65       const QString &srsName;
66 
67       bool hasAxisInverted;
68     };
69 
70     QString createFeatureGeoJSON( const QgsFeature &feature, const createFeatureParams &params, const QgsAttributeList &pkAttributes );
71 
72     QString encodeValueToText( const QVariant &value, const QgsEditorWidgetSetup &setup );
73 
74     QDomElement createFeatureGML2( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams &params, const QgsProject *project, const QgsAttributeList &pkAttributes );
75 
76     QDomElement createFeatureGML3( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams &params, const QgsProject *project, const QgsAttributeList &pkAttributes );
77 
78     void hitGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project,
79                         QgsWfsParameters::Format format, int numberOfFeatures, const QStringList &typeNames, const QgsServerSettings *serverSettings );
80 
81     void startGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project,
82                           QgsWfsParameters::Format format, int prec, QgsCoordinateReferenceSystem &crs,
83                           QgsRectangle *rect, const QStringList &typeNames, const QgsServerSettings *settings );
84 
85     void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, const QgsFeature &feature, int featIdx,
86                         const createFeatureParams &params, const QgsProject *project, const QgsAttributeList &pkAttributes = QgsAttributeList() );
87 
88     void endGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format );
89 
90     QgsServerRequest::Parameters mRequestParameters;
91     QgsWfsParameters mWfsParameters;
92     /* GeoJSON Exporter */
93     QgsJsonExporter mJsonExporter;
94   }
95 
writeGetFeature(QgsServerInterface * serverIface,const QgsProject * project,const QString & version,const QgsServerRequest & request,QgsServerResponse & response)96   void writeGetFeature( QgsServerInterface *serverIface, const QgsProject *project,
97                         const QString &version, const QgsServerRequest &request,
98                         QgsServerResponse &response )
99   {
100     Q_UNUSED( version )
101 
102     mRequestParameters = request.parameters();
103     mWfsParameters = QgsWfsParameters( QUrlQuery( request.url() ) );
104     mWfsParameters.dump();
105     getFeatureRequest aRequest;
106 
107     QDomDocument doc;
108     QString errorMsg;
109 
110     if ( doc.setContent( request.data(), true, &errorMsg ) )
111     {
112       QDomElement docElem = doc.documentElement();
113       aRequest = parseGetFeatureRequestBody( docElem, project );
114     }
115     else
116     {
117       aRequest = parseGetFeatureParameters( project );
118     }
119 
120     // store typeName
121     QStringList typeNameList;
122 
123     // Request metadata
124     bool onlyOneLayer = ( aRequest.queries.size() == 1 );
125     QgsRectangle requestRect;
126     QgsCoordinateReferenceSystem requestCrs;
127     int requestPrecision = 6;
128     if ( !onlyOneLayer )
129       requestCrs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) );
130 
131     QList<getFeatureQuery>::iterator qIt = aRequest.queries.begin();
132     for ( ; qIt != aRequest.queries.end(); ++qIt )
133     {
134       typeNameList << ( *qIt ).typeName;
135     }
136 
137     // get layers and
138     // update the request metadata
139     QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
140     QMap<QString, QgsMapLayer *> mapLayerMap;
141     for ( int i = 0; i < wfsLayerIds.size(); ++i )
142     {
143       QgsMapLayer *layer = project->mapLayer( wfsLayerIds.at( i ) );
144       if ( !layer )
145       {
146         continue;
147       }
148       if ( layer->type() != QgsMapLayerType::VectorLayer )
149       {
150         continue;
151       }
152 
153       QString name = layerTypeName( layer );
154 
155       if ( typeNameList.contains( name ) )
156       {
157         // store layers
158         mapLayerMap[name] = layer;
159         // update request metadata
160         if ( onlyOneLayer )
161         {
162           requestRect = layer->extent();
163           requestCrs = layer->crs();
164         }
165         else
166         {
167           QgsCoordinateTransform transform( layer->crs(), requestCrs, project );
168           try
169           {
170             if ( requestRect.isEmpty() )
171             {
172               requestRect = transform.transform( layer->extent() );
173             }
174             else
175             {
176               requestRect.combineExtentWith( transform.transform( layer->extent() ) );
177             }
178           }
179           catch ( QgsException &cse )
180           {
181             Q_UNUSED( cse )
182             requestRect = QgsRectangle( -180.0, -90.0, 180.0, 90.0 );
183           }
184         }
185       }
186     }
187 
188 #ifdef HAVE_SERVER_PYTHON_PLUGINS
189     QgsAccessControl *accessControl = serverIface->accessControls();
190     //scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
191     //there's LOTS of potential exit paths here, so we avoid having to restore the filters manually
192     std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() );
193 #else
194     ( void )serverIface;
195 #endif
196 
197     // features counters
198     long sentFeatures = 0;
199     long iteratedFeatures = 0;
200     // sent features
201     QgsFeature feature;
202     qIt = aRequest.queries.begin();
203     for ( ; qIt != aRequest.queries.end(); ++qIt )
204     {
205       getFeatureQuery &query = *qIt;
206       QString typeName = query.typeName;
207 
208       if ( !mapLayerMap.contains( typeName ) )
209       {
210         throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' unknown" ).arg( typeName ) );
211       }
212 
213       QgsMapLayer *layer = mapLayerMap[typeName];
214 #ifdef HAVE_SERVER_PYTHON_PLUGINS
215       if ( accessControl && !accessControl->layerReadPermission( layer ) )
216       {
217         throw QgsSecurityAccessException( QStringLiteral( "Feature access permission denied" ) );
218       }
219 #endif
220       QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
221       if ( !vlayer )
222       {
223         throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' layer error" ).arg( typeName ) );
224       }
225 
226       //test provider
227       QgsVectorDataProvider *provider = vlayer->dataProvider();
228       if ( !provider )
229       {
230         throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' layer's provider error" ).arg( typeName ) );
231       }
232 #ifdef HAVE_SERVER_PYTHON_PLUGINS
233       if ( accessControl )
234       {
235         QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, vlayer, filterRestorer->originalFilters() );
236       }
237 #endif
238       //is there alias info for this vector layer?
239       QMap< int, QString > layerAliasInfo;
240       QgsStringMap aliasMap = vlayer->attributeAliases();
241       QgsStringMap::const_iterator aliasIt = aliasMap.constBegin();
242       for ( ; aliasIt != aliasMap.constEnd(); ++aliasIt )
243       {
244         int attrIndex = vlayer->fields().lookupField( aliasIt.key() );
245         if ( attrIndex != -1 )
246         {
247           layerAliasInfo.insert( attrIndex, aliasIt.value() );
248         }
249       }
250 
251       // get propertyList from query
252       const QStringList propertyList = query.propertyList;
253 
254       //Using pending attributes and pending fields
255       QgsAttributeList attrIndexes = vlayer->attributeList();
256       const QgsFields fields = vlayer->fields();
257       bool withGeom = true;
258       if ( !propertyList.isEmpty() && propertyList.first() != QLatin1String( "*" ) )
259       {
260         withGeom = false;
261         QStringList::const_iterator plstIt;
262         QList<int> idxList;
263         // build corresponding propertyname
264         QList<QString> propertynames;
265         QList<QString> fieldnames;
266         for ( const QgsField &field : fields )
267         {
268           fieldnames.append( field.name() );
269           propertynames.append( field.name().replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) );
270         }
271         QString fieldName;
272         for ( plstIt = propertyList.constBegin(); plstIt != propertyList.constEnd(); ++plstIt )
273         {
274           fieldName = *plstIt;
275           int fieldNameIdx = propertynames.indexOf( fieldName );
276           if ( fieldNameIdx == -1 )
277           {
278             fieldNameIdx = fieldnames.indexOf( fieldName );
279           }
280           if ( fieldNameIdx > -1 )
281           {
282             idxList.append( fieldNameIdx );
283           }
284           else if ( fieldName == QLatin1String( "geometry" ) )
285           {
286             withGeom = true;
287           }
288         }
289         if ( !idxList.isEmpty() )
290         {
291           attrIndexes = idxList;
292         }
293       }
294 
295       //excluded attributes for this layer
296       if ( !attrIndexes.isEmpty() )
297       {
298         for ( const QgsField &field : fields )
299         {
300           if ( field.configurationFlags().testFlag( QgsField::ConfigurationFlag::HideFromWfs ) )
301           {
302             int fieldNameIdx = fields.indexOf( field.name() );
303             if ( fieldNameIdx > -1 && attrIndexes.contains( fieldNameIdx ) )
304             {
305               attrIndexes.removeOne( fieldNameIdx );
306             }
307           }
308         }
309       }
310 
311       // update request
312       QgsFeatureRequest featureRequest = query.featureRequest;
313 
314       // expression context
315       QgsExpressionContext expressionContext;
316       expressionContext << QgsExpressionContextUtils::globalScope()
317                         << QgsExpressionContextUtils::projectScope( project )
318                         << QgsExpressionContextUtils::layerScope( vlayer );
319       featureRequest.setExpressionContext( expressionContext );
320 
321       if ( !query.serverFids.isEmpty() )
322       {
323         QgsServerFeatureId::updateFeatureRequestFromServerFids( featureRequest, query.serverFids, provider );
324       }
325 
326       // geometry flags
327       if ( vlayer->wkbType() == QgsWkbTypes::NoGeometry )
328         featureRequest.setFlags( featureRequest.flags() | QgsFeatureRequest::NoGeometry );
329       else
330         featureRequest.setFlags( featureRequest.flags() | ( withGeom ? QgsFeatureRequest::NoFlags : QgsFeatureRequest::NoGeometry ) );
331 
332       // subset of attributes
333       featureRequest.setSubsetOfAttributes( attrIndexes );
334 #ifdef HAVE_SERVER_PYTHON_PLUGINS
335       if ( accessControl )
336       {
337         accessControl->filterFeatures( vlayer, featureRequest );
338 
339         QStringList attributes = QStringList();
340         for ( int idx : std::as_const( attrIndexes ) )
341         {
342           attributes.append( vlayer->fields().field( idx ).name() );
343         }
344         featureRequest.setSubsetOfAttributes(
345           accessControl->layerAttributes( vlayer, attributes ),
346           vlayer->fields() );
347         attrIndexes = featureRequest.subsetOfAttributes();
348       }
349 #endif
350 
351       // Force pkAttributes in subset of attributes for primary fid building
352       const QgsAttributeList pkAttributes = provider->pkAttributeIndexes();
353       if ( !pkAttributes.isEmpty() )
354       {
355         QgsAttributeList subsetOfAttrs = featureRequest.subsetOfAttributes();
356         for ( int idx : pkAttributes )
357         {
358           if ( !subsetOfAttrs.contains( idx ) )
359           {
360             subsetOfAttrs.prepend( idx );
361           }
362         }
363         if ( subsetOfAttrs.size() != featureRequest.subsetOfAttributes().size() )
364         {
365           featureRequest.setSubsetOfAttributes( subsetOfAttrs );
366         }
367       }
368 
369       if ( onlyOneLayer )
370       {
371         requestPrecision = QgsServerProjectUtils::wfsLayerPrecision( *project, vlayer->id() );
372       }
373 
374       if ( aRequest.maxFeatures > 0 )
375       {
376         featureRequest.setLimit( aRequest.maxFeatures + aRequest.startIndex - sentFeatures );
377       }
378       // specific layer precision
379       int layerPrecision = QgsServerProjectUtils::wfsLayerPrecision( *project, vlayer->id() );
380       // specific layer crs
381       QgsCoordinateReferenceSystem layerCrs = vlayer->crs();
382 
383       // Geometry name
384       QString geometryName = aRequest.geometryName;
385       if ( !withGeom )
386       {
387         geometryName = QLatin1String( "NONE" );
388       }
389       // outputCrs
390       QgsCoordinateReferenceSystem outputCrs = vlayer->crs();
391       if ( !query.srsName.isEmpty() )
392       {
393         outputCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( query.srsName );
394       }
395 
396       bool forceGeomToMulti = QgsWkbTypes::isMultiType( vlayer->wkbType() );
397 
398       if ( !featureRequest.filterRect().isEmpty() )
399       {
400         QgsCoordinateTransform transform( outputCrs, vlayer->crs(), project );
401         try
402         {
403           featureRequest.setFilterRect( transform.transform( featureRequest.filterRect() ) );
404         }
405         catch ( QgsException &cse )
406         {
407           Q_UNUSED( cse )
408         }
409         if ( onlyOneLayer )
410         {
411           requestRect = featureRequest.filterRect();
412         }
413       }
414 
415       // Iterate through features
416       QgsFeatureIterator fit = vlayer->getFeatures( featureRequest );
417 
418       if ( mWfsParameters.resultType() == QgsWfsParameters::ResultType::HITS )
419       {
420         while ( fit.nextFeature( feature ) && ( aRequest.maxFeatures == -1 || sentFeatures < aRequest.maxFeatures ) )
421         {
422           if ( iteratedFeatures >= aRequest.startIndex )
423           {
424             ++sentFeatures;
425           }
426           ++iteratedFeatures;
427         }
428       }
429       else
430       {
431 
432         // For WFS 1.1 we honor requested CRS and axis order
433         const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )};
434         const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) &&
435                                 outputCrs.hasAxisInverted() &&
436                                 ! srsName.startsWith( QLatin1String( "EPSG:" ) ) };
437 
438         const createFeatureParams cfp = { layerPrecision,
439                                           layerCrs,
440                                           attrIndexes,
441                                           typeName,
442                                           withGeom,
443                                           geometryName,
444                                           outputCrs,
445                                           forceGeomToMulti,
446                                           srsName,
447                                           invertAxis
448                                         };
449         while ( fit.nextFeature( feature ) && ( aRequest.maxFeatures == -1 || sentFeatures < aRequest.maxFeatures ) )
450         {
451           if ( iteratedFeatures == aRequest.startIndex )
452             startGetFeature( request, response, project, aRequest.outputFormat, requestPrecision, requestCrs, &requestRect, typeNameList, serverIface->serverSettings() );
453 
454           if ( iteratedFeatures >= aRequest.startIndex )
455           {
456             setGetFeature( response, aRequest.outputFormat, feature, sentFeatures, cfp, project, provider->pkAttributeIndexes() );
457             ++sentFeatures;
458           }
459           ++iteratedFeatures;
460         }
461       }
462     }
463 
464 #ifdef HAVE_SERVER_PYTHON_PLUGINS
465     //force restoration of original layer filters
466     filterRestorer.reset();
467 #endif
468 
469     if ( mWfsParameters.resultType() == QgsWfsParameters::ResultType::HITS )
470     {
471       hitGetFeature( request, response, project, aRequest.outputFormat, sentFeatures, typeNameList, serverIface->serverSettings() );
472     }
473     else
474     {
475       // End of GetFeature
476       if ( iteratedFeatures <= aRequest.startIndex )
477         startGetFeature( request, response, project, aRequest.outputFormat, requestPrecision, requestCrs, &requestRect, typeNameList, serverIface->serverSettings() );
478       endGetFeature( response, aRequest.outputFormat );
479     }
480 
481   }
482 
parseGetFeatureParameters(const QgsProject * project)483   getFeatureRequest parseGetFeatureParameters( const QgsProject *project )
484   {
485     getFeatureRequest request;
486     request.maxFeatures = mWfsParameters.maxFeaturesAsInt();
487     request.startIndex = mWfsParameters.startIndexAsInt();
488     request.outputFormat = mWfsParameters.outputFormat();
489 
490     // Verifying parameters mutually exclusive
491     QStringList fidList = mWfsParameters.featureIds();
492     bool paramContainsFeatureIds = !fidList.isEmpty();
493     QStringList filterList = mWfsParameters.filters();
494     bool paramContainsFilters = !filterList.isEmpty();
495     QString bbox = mWfsParameters.bbox();
496     bool paramContainsBbox = !bbox.isEmpty();
497     if ( ( paramContainsFeatureIds
498            && ( paramContainsFilters || paramContainsBbox ) )
499          || ( paramContainsFilters
500               && ( paramContainsFeatureIds || paramContainsBbox ) )
501          || ( paramContainsBbox
502               && ( paramContainsFeatureIds || paramContainsFilters ) )
503        )
504     {
505       throw QgsRequestNotWellFormedException( QStringLiteral( "FEATUREID FILTER and BBOX parameters are mutually exclusive" ) );
506     }
507 
508     // Get and split PROPERTYNAME parameter
509     QStringList propertyNameList = mWfsParameters.propertyNames();
510 
511     // Manage extra parameter GeometryName
512     request.geometryName = mWfsParameters.geometryNameAsString().toUpper();
513 
514     QStringList typeNameList;
515     // parse FEATUREID
516     if ( paramContainsFeatureIds )
517     {
518       // Verifying the 1:1 mapping between FEATUREID and PROPERTYNAME
519       if ( !propertyNameList.isEmpty() && propertyNameList.size() != fidList.size() )
520       {
521         throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a FEATUREID and the PROPERTYNAME list" ) );
522       }
523       if ( propertyNameList.isEmpty() )
524       {
525         for ( int i = 0; i < fidList.size(); ++i )
526         {
527           propertyNameList << QStringLiteral( "*" );
528         }
529       }
530 
531       QMap<QString, QStringList> fidsMap;
532 
533       QStringList::const_iterator fidIt = fidList.constBegin();
534       QStringList::const_iterator propertyNameIt = propertyNameList.constBegin();
535       for ( ; fidIt != fidList.constEnd(); ++fidIt )
536       {
537         // Get FeatureID
538         QString fid = *fidIt;
539         fid = fid.trimmed();
540         // Get PropertyName for this FeatureID
541         QString propertyName;
542         if ( propertyNameIt != propertyNameList.constEnd() )
543         {
544           propertyName = *propertyNameIt;
545         }
546         // testing typename in the WFS featureID
547         if ( !fid.contains( '.' ) )
548         {
549           throw QgsRequestNotWellFormedException( QStringLiteral( "FEATUREID has to have TYPENAME in the values" ) );
550         }
551 
552         QString typeName = fid.section( '.', 0, 0 );
553         fid = fid.section( '.', 1, 1 );
554         if ( !typeNameList.contains( typeName ) )
555         {
556           typeNameList << typeName;
557         }
558 
559         // each Feature requested by FEATUREID can have each own property list
560         QString key = QStringLiteral( "%1(%2)" ).arg( typeName, propertyName );
561         QStringList fids;
562         if ( fidsMap.contains( key ) )
563         {
564           fids = fidsMap.value( key );
565         }
566         fids.append( fid );
567         fidsMap.insert( key, fids );
568 
569         if ( propertyNameIt != propertyNameList.constEnd() )
570         {
571           ++propertyNameIt;
572         }
573       }
574 
575       QMap<QString, QStringList>::const_iterator fidsMapIt = fidsMap.constBegin();
576       while ( fidsMapIt != fidsMap.constEnd() )
577       {
578         QString key = fidsMapIt.key();
579 
580         //Extract TypeName and PropertyName from key
581         QRegExp rx( "([^()]+)\\(([^()]+)\\)" );
582         if ( rx.indexIn( key, 0 ) == -1 )
583         {
584           throw QgsRequestNotWellFormedException( QStringLiteral( "Error getting properties for FEATUREID" ) );
585         }
586         QString typeName = rx.cap( 1 );
587         QString propertyName = rx.cap( 2 );
588 
589         getFeatureQuery query;
590         query.typeName = typeName;
591         query.srsName = mWfsParameters.srsName();
592 
593         // Parse PropertyName
594         if ( propertyName != QLatin1String( "*" ) )
595         {
596           QStringList propertyList;
597 
598           const QStringList attrList = propertyName.split( ',' );
599           QStringList::const_iterator alstIt;
600           for ( alstIt = attrList.constBegin(); alstIt != attrList.constEnd(); ++alstIt )
601           {
602             QString fieldName = *alstIt;
603             fieldName = fieldName.trimmed();
604             if ( fieldName.contains( ':' ) )
605             {
606               fieldName = fieldName.section( ':', 1, 1 );
607             }
608             if ( fieldName.contains( '/' ) )
609             {
610               if ( fieldName.section( '/', 0, 0 ) != typeName )
611               {
612                 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) );
613               }
614               fieldName = fieldName.section( '/', 1, 1 );
615             }
616             propertyList.append( fieldName );
617           }
618           query.propertyList = propertyList;
619         }
620 
621         query.serverFids = fidsMapIt.value();
622         QgsFeatureRequest featureRequest;
623 
624         query.featureRequest = featureRequest;
625         request.queries.append( query );
626         ++fidsMapIt;
627       }
628       return request;
629     }
630 
631     if ( !mRequestParameters.contains( QStringLiteral( "TYPENAME" ) ) )
632     {
633       throw QgsRequestNotWellFormedException( QStringLiteral( "TYPENAME is mandatory except if FEATUREID is used" ) );
634     }
635 
636     typeNameList = mWfsParameters.typeNames();
637     // Verifying the 1:1 mapping between TYPENAME and PROPERTYNAME
638     if ( !propertyNameList.isEmpty() && typeNameList.size() != propertyNameList.size() )
639     {
640       throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a TYPENAME and the PROPERTYNAME list" ) );
641     }
642     if ( propertyNameList.isEmpty() )
643     {
644       for ( int i = 0; i < typeNameList.size(); ++i )
645       {
646         propertyNameList << QStringLiteral( "*" );
647       }
648     }
649 
650     // Create queries based on TypeName and propertyName
651     QStringList::const_iterator typeNameIt = typeNameList.constBegin();
652     QStringList::const_iterator propertyNameIt = propertyNameList.constBegin();
653     for ( ; typeNameIt != typeNameList.constEnd(); ++typeNameIt )
654     {
655       QString typeName = *typeNameIt;
656       typeName = typeName.trimmed();
657       // Get PropertyName for this typeName
658       QString propertyName;
659       if ( propertyNameIt != propertyNameList.constEnd() )
660       {
661         propertyName = *propertyNameIt;
662       }
663 
664       getFeatureQuery query;
665       query.typeName = typeName;
666       query.srsName = mWfsParameters.srsName();
667 
668       // Parse PropertyName
669       if ( propertyName != QLatin1String( "*" ) )
670       {
671         QStringList propertyList;
672 
673         const QStringList attrList = propertyName.split( ',' );
674         QStringList::const_iterator alstIt;
675         for ( alstIt = attrList.constBegin(); alstIt != attrList.constEnd(); ++alstIt )
676         {
677           QString fieldName = *alstIt;
678           fieldName = fieldName.trimmed();
679           if ( fieldName.contains( ':' ) )
680           {
681             fieldName = fieldName.section( ':', 1, 1 );
682           }
683           if ( fieldName.contains( '/' ) )
684           {
685             if ( fieldName.section( '/', 0, 0 ) != typeName )
686             {
687               throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) );
688             }
689             fieldName = fieldName.section( '/', 1, 1 );
690           }
691           propertyList.append( fieldName );
692         }
693         query.propertyList = propertyList;
694       }
695 
696       request.queries.append( query );
697 
698       if ( propertyNameIt != propertyNameList.constEnd() )
699       {
700         ++propertyNameIt;
701       }
702     }
703 
704     // Manage extra parameter exp_filter
705     QStringList expFilterList = mWfsParameters.expFilters();
706     if ( !expFilterList.isEmpty() )
707     {
708       // Verifying the 1:1 mapping between TYPENAME and EXP_FILTER but without exception
709       if ( request.queries.size() == expFilterList.size() )
710       {
711         // set feature request filter expression based on filter element
712         QList<getFeatureQuery>::iterator qIt = request.queries.begin();
713         QStringList::const_iterator expFilterIt = expFilterList.constBegin();
714         for ( ; qIt != request.queries.end(); ++qIt )
715         {
716           getFeatureQuery &query = *qIt;
717           // Get Filter for this typeName
718           QString expFilter;
719           if ( expFilterIt != expFilterList.constEnd() )
720           {
721             expFilter = *expFilterIt;
722           }
723           std::shared_ptr<QgsExpression> filter( new QgsExpression( expFilter ) );
724           if ( filter )
725           {
726             if ( filter->hasParserError() )
727             {
728               throw QgsRequestNotWellFormedException( QStringLiteral( "The EXP_FILTER expression has errors: %1" ).arg( filter->parserErrorString() ) );
729             }
730             if ( filter->needsGeometry() )
731             {
732               query.featureRequest.setFlags( QgsFeatureRequest::NoFlags );
733             }
734             query.featureRequest.setFilterExpression( filter->expression() );
735           }
736         }
737       }
738       else
739       {
740         QgsMessageLog::logMessage( "There has to be a 1:1 mapping between each element in a TYPENAME and the EXP_FILTER list" );
741       }
742     }
743 
744     if ( paramContainsBbox )
745     {
746 
747       // get bbox extent
748       QgsRectangle extent = mWfsParameters.bboxAsRectangle();
749 
750       QString extentSrsName { mWfsParameters.srsName() };
751 
752       // handle WFS 1.1.0 optional CRS
753       if ( mWfsParameters.bbox().split( ',' ).size() == 5 && ! mWfsParameters.srsName().isEmpty() )
754       {
755         QString crs( mWfsParameters.bbox().split( ',' )[4] );
756         if ( crs != mWfsParameters.srsName() )
757         {
758           extentSrsName = crs;
759           QgsCoordinateReferenceSystem sourceCrs( crs );
760           QgsCoordinateReferenceSystem destinationCrs( mWfsParameters.srsName() );
761           if ( sourceCrs.isValid() && destinationCrs.isValid( ) )
762           {
763             QgsGeometry extentGeom = QgsGeometry::fromRect( extent );
764             QgsCoordinateTransform transform;
765             transform.setSourceCrs( sourceCrs );
766             transform.setDestinationCrs( destinationCrs );
767             try
768             {
769               if ( extentGeom.transform( transform ) == Qgis::GeometryOperationResult::Success )
770               {
771                 extent = QgsRectangle( extentGeom.boundingBox() );
772               }
773             }
774             catch ( QgsException &cse )
775             {
776               Q_UNUSED( cse )
777             }
778           }
779         }
780       }
781 
782       // Follow GeoServer conventions and handle axis order
783       // See: https://docs.geoserver.org/latest/en/user/services/wfs/axis_order.html#wfs-basics-axis
784       QgsCoordinateReferenceSystem extentCrs;
785       extentCrs.createFromUserInput( extentSrsName );
786       if ( extentCrs.isValid() && extentCrs.hasAxisInverted() && ! extentSrsName.startsWith( QLatin1String( "EPSG:" ) ) )
787       {
788         QgsGeometry geom { QgsGeometry::fromRect( extent ) };
789         geom.get()->swapXy();
790         extent = geom.boundingBox();
791       }
792 
793       // set feature request filter rectangle
794       QList<getFeatureQuery>::iterator qIt = request.queries.begin();
795       for ( ; qIt != request.queries.end(); ++qIt )
796       {
797         getFeatureQuery &query = *qIt;
798         query.featureRequest.setFilterRect( extent ).setFlags( query.featureRequest.flags() | QgsFeatureRequest::ExactIntersect );
799       }
800       return request;
801     }
802     else if ( paramContainsFilters )
803     {
804       // Verifying the 1:1 mapping between TYPENAME and FILTER
805       if ( request.queries.size() != filterList.size() )
806       {
807         throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a TYPENAME and the FILTER list" ) );
808       }
809 
810       // set feature request filter expression based on filter element
811       QList<getFeatureQuery>::iterator qIt = request.queries.begin();
812       QStringList::const_iterator filterIt = filterList.constBegin();
813       for ( ; qIt != request.queries.end(); ++qIt )
814       {
815         getFeatureQuery &query = *qIt;
816         // Get Filter for this typeName
817         QDomDocument filter;
818         if ( filterIt != filterList.constEnd() )
819         {
820           QString errorMsg;
821           if ( !filter.setContent( *filterIt, true, &errorMsg ) )
822           {
823             throw QgsRequestNotWellFormedException( QStringLiteral( "error message: %1. The XML string was: %2" ).arg( errorMsg, *filterIt ) );
824           }
825         }
826 
827         QDomElement filterElem = filter.firstChildElement();
828         QStringList serverFids;
829         query.featureRequest = parseFilterElement( query.typeName, filterElem, serverFids, project );
830         query.serverFids = serverFids;
831 
832         if ( filterIt != filterList.constEnd() )
833         {
834           ++filterIt;
835         }
836       }
837       return request;
838     }
839 
840     QStringList sortByList = mWfsParameters.sortBy();
841     if ( !sortByList.isEmpty() && request.queries.size() == sortByList.size() )
842     {
843       // add order by to feature request
844       QList<getFeatureQuery>::iterator qIt = request.queries.begin();
845       QStringList::const_iterator sortByIt = sortByList.constBegin();
846       for ( ; qIt != request.queries.end(); ++qIt )
847       {
848         getFeatureQuery &query = *qIt;
849         // Get sortBy for this typeName
850         QString sortBy;
851         if ( sortByIt != sortByList.constEnd() )
852         {
853           sortBy = *sortByIt;
854         }
855         for ( const QString &attribute : sortBy.split( ',' ) )
856         {
857           if ( attribute.endsWith( QLatin1String( " D" ) ) || attribute.endsWith( QLatin1String( "+D" ) ) )
858           {
859             query.featureRequest.addOrderBy( attribute.left( attribute.size() - 2 ), false );
860           }
861           else if ( attribute.endsWith( QLatin1String( " DESC" ) ) || attribute.endsWith( QLatin1String( "+DESC" ) ) )
862           {
863             query.featureRequest.addOrderBy( attribute.left( attribute.size() - 5 ), false );
864           }
865           else if ( attribute.endsWith( QLatin1String( " A" ) ) || attribute.endsWith( QLatin1String( "+A" ) ) )
866           {
867             query.featureRequest.addOrderBy( attribute.left( attribute.size() - 2 ) );
868           }
869           else if ( attribute.endsWith( QLatin1String( " ASC" ) ) || attribute.endsWith( QLatin1String( "+ASC" ) ) )
870           {
871             query.featureRequest.addOrderBy( attribute.left( attribute.size() - 4 ) );
872           }
873           else
874           {
875             query.featureRequest.addOrderBy( attribute );
876           }
877         }
878       }
879     }
880 
881     return request;
882   }
883 
parseGetFeatureRequestBody(QDomElement & docElem,const QgsProject * project)884   getFeatureRequest parseGetFeatureRequestBody( QDomElement &docElem, const QgsProject *project )
885   {
886     getFeatureRequest request;
887     request.maxFeatures = mWfsParameters.maxFeaturesAsInt();
888     request.startIndex = mWfsParameters.startIndexAsInt();
889     request.outputFormat = mWfsParameters.outputFormat();
890 
891     QDomNodeList queryNodes = docElem.elementsByTagName( QStringLiteral( "Query" ) );
892     QDomElement queryElem;
893     for ( int i = 0; i < queryNodes.size(); i++ )
894     {
895       queryElem = queryNodes.at( i ).toElement();
896       getFeatureQuery query = parseQueryElement( queryElem, project );
897       request.queries.append( query );
898     }
899     return request;
900   }
901 
parseSortByElement(QDomElement & sortByElem,QgsFeatureRequest & featureRequest,const QString & typeName)902   void parseSortByElement( QDomElement &sortByElem, QgsFeatureRequest &featureRequest, const QString &typeName )
903   {
904     QDomNodeList sortByNodes = sortByElem.childNodes();
905     if ( sortByNodes.size() )
906     {
907       for ( int i = 0; i < sortByNodes.size(); i++ )
908       {
909         QDomElement sortPropElem = sortByNodes.at( i ).toElement();
910         QDomNodeList sortPropChildNodes = sortPropElem.childNodes();
911         if ( sortPropChildNodes.size() )
912         {
913           QString fieldName;
914           bool ascending = true;
915           for ( int j = 0; j < sortPropChildNodes.size(); j++ )
916           {
917             QDomElement sortPropChildElem = sortPropChildNodes.at( j ).toElement();
918             if ( sortPropChildElem.tagName() == QLatin1String( "PropertyName" ) )
919             {
920               fieldName = sortPropChildElem.text().trimmed();
921             }
922             else if ( sortPropChildElem.tagName() == QLatin1String( "SortOrder" ) )
923             {
924               QString sortOrder = sortPropChildElem.text().trimmed().toUpper();
925               if ( sortOrder == QLatin1String( "DESC" ) || sortOrder == QLatin1String( "D" ) )
926                 ascending = false;
927             }
928           }
929           // clean fieldName
930           if ( fieldName.contains( ':' ) )
931           {
932             fieldName = fieldName.section( ':', 1, 1 );
933           }
934           if ( fieldName.contains( '/' ) )
935           {
936             if ( fieldName.section( '/', 0, 0 ) != typeName )
937             {
938               throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) );
939             }
940             fieldName = fieldName.section( '/', 1, 1 );
941           }
942           // addOrderBy
943           if ( !fieldName.isEmpty() )
944             featureRequest.addOrderBy( fieldName, ascending );
945         }
946       }
947     }
948   }
949 
parseQueryElement(QDomElement & queryElem,const QgsProject * project)950   getFeatureQuery parseQueryElement( QDomElement &queryElem, const QgsProject *project )
951   {
952     QString typeName = queryElem.attribute( QStringLiteral( "typeName" ), QString() );
953     if ( typeName.contains( ':' ) )
954     {
955       typeName = typeName.section( ':', 1, 1 );
956     }
957 
958     QgsFeatureRequest featureRequest;
959     QStringList serverFids;
960     QStringList propertyList;
961     QDomNodeList queryChildNodes = queryElem.childNodes();
962     if ( queryChildNodes.size() )
963     {
964       QDomElement sortByElem;
965       for ( int q = 0; q < queryChildNodes.size(); q++ )
966       {
967         QDomElement queryChildElem = queryChildNodes.at( q ).toElement();
968         if ( queryChildElem.tagName() == QLatin1String( "PropertyName" ) )
969         {
970           QString fieldName = queryChildElem.text().trimmed();
971           if ( fieldName.contains( ':' ) )
972           {
973             fieldName = fieldName.section( ':', 1, 1 );
974           }
975           if ( fieldName.contains( '/' ) )
976           {
977             if ( fieldName.section( '/', 0, 0 ) != typeName )
978             {
979               throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) );
980             }
981             fieldName = fieldName.section( '/', 1, 1 );
982           }
983           propertyList.append( fieldName );
984         }
985         else if ( queryChildElem.tagName() == QLatin1String( "Filter" ) )
986         {
987           featureRequest = parseFilterElement( typeName, queryChildElem, serverFids, project );
988         }
989         else if ( queryChildElem.tagName() == QLatin1String( "SortBy" ) )
990         {
991           sortByElem = queryChildElem;
992         }
993       }
994       parseSortByElement( sortByElem, featureRequest, typeName );
995     }
996 
997     // srsName attribute
998     QString srsName = queryElem.attribute( QStringLiteral( "srsName" ), QString() );
999 
1000     getFeatureQuery query;
1001     query.typeName = typeName;
1002     query.srsName = srsName;
1003     query.featureRequest = featureRequest;
1004     query.serverFids = serverFids;
1005     query.propertyList = propertyList;
1006     return query;
1007   }
1008 
1009   namespace
1010   {
1011     static QSet< QString > sParamFilter
1012     {
1013       QStringLiteral( "REQUEST" ),
1014       QStringLiteral( "FORMAT" ),
1015       QStringLiteral( "OUTPUTFORMAT" ),
1016       QStringLiteral( "BBOX" ),
1017       QStringLiteral( "FEATUREID" ),
1018       QStringLiteral( "TYPENAME" ),
1019       QStringLiteral( "FILTER" ),
1020       QStringLiteral( "EXP_FILTER" ),
1021       QStringLiteral( "MAXFEATURES" ),
1022       QStringLiteral( "STARTINDEX" ),
1023       QStringLiteral( "PROPERTYNAME" ),
1024       QStringLiteral( "_DC" )
1025     };
1026 
1027 
hitGetFeature(const QgsServerRequest & request,QgsServerResponse & response,const QgsProject * project,QgsWfsParameters::Format format,int numberOfFeatures,const QStringList & typeNames,const QgsServerSettings * settings)1028     void hitGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, QgsWfsParameters::Format format,
1029                         int numberOfFeatures, const QStringList &typeNames, const QgsServerSettings *settings )
1030     {
1031       QDateTime now = QDateTime::currentDateTime();
1032       QString fcString;
1033 
1034       if ( format == QgsWfsParameters::Format::GeoJSON )
1035       {
1036         response.setHeader( "Content-Type", "application/vnd.geo+json; charset=utf-8" );
1037         fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" );
1038         fcString += QStringLiteral( " \"timeStamp\": \"%1\"\n" ).arg( now.toString( Qt::ISODate ) );
1039         fcString += QStringLiteral( " \"numberOfFeatures\": %1\n" ).arg( QString::number( numberOfFeatures ) );
1040         fcString += QLatin1Char( '}' );
1041       }
1042       else
1043       {
1044         if ( format == QgsWfsParameters::Format::GML2 )
1045           response.setHeader( "Content-Type", "text/xml; subtype=gml/2.1.2; charset=utf-8" );
1046         else
1047           response.setHeader( "Content-Type", "text/xml; subtype=gml/3.1.1; charset=utf-8" );
1048 
1049         //Prepare url
1050         QString hrefString = serviceUrl( request, project, *settings );
1051 
1052         QUrl mapUrl( hrefString );
1053 
1054         QUrlQuery query( mapUrl );
1055         query.addQueryItem( QStringLiteral( "SERVICE" ), QStringLiteral( "WFS" ) );
1056         //Set version
1057         if ( mWfsParameters.version().isEmpty() )
1058           query.addQueryItem( QStringLiteral( "VERSION" ), implementationVersion() );
1059         else if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) )
1060           query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.1.0" ) );
1061         else
1062           query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) );
1063 
1064         for ( auto param : query.queryItems() )
1065         {
1066           if ( sParamFilter.contains( param.first.toUpper() ) )
1067             query.removeAllQueryItems( param.first );
1068         }
1069 
1070         query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) );
1071         query.addQueryItem( QStringLiteral( "TYPENAME" ), typeNames.join( ',' ) );
1072         if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) )
1073         {
1074           if ( format == QgsWfsParameters::Format::GML2 )
1075             query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/2.1.2" ) );
1076           else
1077             query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/3.1.1" ) );
1078         }
1079         else
1080           query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "XMLSCHEMA" ) );
1081 
1082         mapUrl.setQuery( query );
1083 
1084         hrefString = mapUrl.toString();
1085 
1086         QString wfsSchema;
1087         if ( mWfsParameters.version().isEmpty() || mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) )
1088           wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.1.0/wfs.xsd" );
1089         else
1090           wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.0.0/wfs.xsd" );
1091 
1092         //wfs:FeatureCollection valid
1093         fcString = QStringLiteral( "<wfs:FeatureCollection" );
1094         fcString += " xmlns:wfs=\"" + WFS_NAMESPACE + "\"";
1095         fcString += " xmlns:ogc=\"" + OGC_NAMESPACE + "\"";
1096         fcString += " xmlns:gml=\"" + GML_NAMESPACE + "\"";
1097         fcString += QLatin1String( " xmlns:ows=\"http://www.opengis.net/ows\"" );
1098         fcString += QLatin1String( " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" );
1099         fcString += " xmlns:qgs=\"" + QGS_NAMESPACE + "\"";
1100         fcString += QLatin1String( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" );
1101         fcString += " xsi:schemaLocation=\"" + WFS_NAMESPACE + " " + wfsSchema + " " + QGS_NAMESPACE + " " + hrefString.replace( QLatin1String( "&" ), QLatin1String( "&amp;" ) ) + "\"";
1102         fcString += "\n timeStamp=\"" + now.toString( Qt::ISODate ) + "\"";
1103         fcString += "\n numberOfFeatures=\"" + QString::number( numberOfFeatures ) + "\"";
1104         fcString += QLatin1String( ">\n" );
1105         fcString += QLatin1String( "</wfs:FeatureCollection>" );
1106       }
1107 
1108       response.write( fcString.toUtf8() );
1109       response.flush();
1110     }
1111 
startGetFeature(const QgsServerRequest & request,QgsServerResponse & response,const QgsProject * project,QgsWfsParameters::Format format,int prec,QgsCoordinateReferenceSystem & crs,QgsRectangle * rect,const QStringList & typeNames,const QgsServerSettings * settings)1112     void startGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, QgsWfsParameters::Format format,
1113                           int prec, QgsCoordinateReferenceSystem &crs, QgsRectangle *rect, const QStringList &typeNames, const QgsServerSettings *settings )
1114     {
1115       QString fcString;
1116 
1117       std::unique_ptr< QgsRectangle > transformedRect;
1118 
1119       if ( format == QgsWfsParameters::Format::GeoJSON )
1120       {
1121         response.setHeader( "Content-Type", "application/vnd.geo+json; charset=utf-8" );
1122 
1123         if ( crs.isValid() && !rect->isEmpty() )
1124         {
1125           QgsGeometry exportGeom = QgsGeometry::fromRect( *rect );
1126           QgsCoordinateTransform transform;
1127           transform.setSourceCrs( crs );
1128           transform.setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
1129           try
1130           {
1131             if ( exportGeom.transform( transform ) == Qgis::GeometryOperationResult::Success )
1132             {
1133               transformedRect.reset( new QgsRectangle( exportGeom.boundingBox() ) );
1134               rect = transformedRect.get();
1135             }
1136           }
1137           catch ( QgsException &cse )
1138           {
1139             Q_UNUSED( cse )
1140           }
1141         }
1142         // EPSG:4326 max extent is -180, -90, 180, 90
1143         rect = new QgsRectangle( rect->intersect( QgsRectangle( -180.0, -90.0, 180.0, 90.0 ) ) );
1144 
1145         fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" );
1146         fcString += " \"bbox\": [ " + qgsDoubleToString( rect->xMinimum(), prec ) + ", " + qgsDoubleToString( rect->yMinimum(), prec ) + ", " + qgsDoubleToString( rect->xMaximum(), prec ) + ", " + qgsDoubleToString( rect->yMaximum(), prec ) + "],\n";
1147         fcString += QLatin1String( " \"features\": [\n" );
1148         response.write( fcString.toUtf8() );
1149       }
1150       else
1151       {
1152         if ( format == QgsWfsParameters::Format::GML2 )
1153           response.setHeader( "Content-Type", "text/xml; subtype=gml/2.1.2; charset=utf-8" );
1154         else
1155           response.setHeader( "Content-Type", "text/xml; subtype=gml/3.1.1; charset=utf-8" );
1156 
1157         //Prepare url
1158         QString hrefString = serviceUrl( request, project, *settings );
1159 
1160         QUrl mapUrl( hrefString );
1161 
1162         QUrlQuery query( mapUrl );
1163         query.addQueryItem( QStringLiteral( "SERVICE" ), QStringLiteral( "WFS" ) );
1164         //Set version
1165         if ( mWfsParameters.version().isEmpty() )
1166           query.addQueryItem( QStringLiteral( "VERSION" ), implementationVersion() );
1167         else if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) )
1168           query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.1.0" ) );
1169         else
1170           query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) );
1171 
1172         const auto queryItems {query.queryItems()};
1173         for ( auto param : std::as_const( queryItems ) )
1174         {
1175           if ( sParamFilter.contains( param.first.toUpper() ) )
1176             query.removeAllQueryItems( param.first );
1177         }
1178 
1179         query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) );
1180         query.addQueryItem( QStringLiteral( "TYPENAME" ), typeNames.join( ',' ) );
1181         if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) )
1182         {
1183           if ( format == QgsWfsParameters::Format::GML2 )
1184             query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/2.1.2" ) );
1185           else
1186             query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/3.1.1" ) );
1187         }
1188         else
1189           query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "XMLSCHEMA" ) );
1190 
1191         mapUrl.setQuery( query );
1192 
1193         hrefString = mapUrl.toString();
1194 
1195         QString wfsSchema;
1196         if ( mWfsParameters.version().isEmpty() || mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) )
1197           wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.1.0/wfs.xsd" );
1198         else
1199           wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.0.0/wfs.xsd" );
1200 
1201         //wfs:FeatureCollection valid
1202         fcString = QStringLiteral( "<wfs:FeatureCollection" );
1203         fcString += " xmlns:wfs=\"" + WFS_NAMESPACE + "\"";
1204         fcString += " xmlns:ogc=\"" + OGC_NAMESPACE + "\"";
1205         fcString += " xmlns:gml=\"" + GML_NAMESPACE + "\"";
1206         fcString += QLatin1String( " xmlns:ows=\"http://www.opengis.net/ows\"" );
1207         fcString += QLatin1String( " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" );
1208         fcString += " xmlns:qgs=\"" + QGS_NAMESPACE + "\"";
1209         fcString += QLatin1String( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" );
1210         fcString += " xsi:schemaLocation=\"" + WFS_NAMESPACE + " " + wfsSchema + " " + QGS_NAMESPACE + " " + hrefString.replace( QLatin1String( "&" ), QLatin1String( "&amp;" ) ) + "\"";
1211         fcString += QLatin1String( ">\n" );
1212 
1213         response.write( fcString.toUtf8() );
1214         response.flush();
1215 
1216         QDomDocument doc;
1217         QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) );
1218         if ( format == QgsWfsParameters::Format::GML3 )
1219         {
1220           // For WFS 1.1 we honor requested CRS and axis order
1221           const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )};
1222           const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) &&
1223                                   crs.hasAxisInverted() &&
1224                                   ! srsName.startsWith( QLatin1String( "EPSG:" ) ) };
1225           QDomElement envElem = QgsOgcUtils::rectangleToGMLEnvelope( rect, doc, srsName, invertAxis, prec );
1226           if ( !envElem.isNull() )
1227           {
1228             if ( crs.isValid() )
1229             {
1230               if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) )
1231               {
1232                 envElem.setAttribute( QStringLiteral( "srsName" ), srsName );
1233               }
1234               else
1235               {
1236                 envElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() );
1237               }
1238             }
1239             bbElem.appendChild( envElem );
1240             doc.appendChild( bbElem );
1241           }
1242         }
1243         else
1244         {
1245           QDomElement boxElem = QgsOgcUtils::rectangleToGMLBox( rect, doc, prec );
1246           if ( !boxElem.isNull() )
1247           {
1248             if ( crs.isValid() )
1249             {
1250               boxElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() );
1251             }
1252             bbElem.appendChild( boxElem );
1253             doc.appendChild( bbElem );
1254           }
1255         }
1256         response.write( doc.toByteArray() );
1257         response.flush();
1258       }
1259     }
1260 
setGetFeature(QgsServerResponse & response,QgsWfsParameters::Format format,const QgsFeature & feature,int featIdx,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1261     void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, const QgsFeature &feature, int featIdx,
1262                         const createFeatureParams &params, const QgsProject *project, const QgsAttributeList &pkAttributes )
1263     {
1264       if ( !feature.isValid() )
1265         return;
1266 
1267       if ( format == QgsWfsParameters::Format::GeoJSON )
1268       {
1269         QString fcString;
1270         if ( featIdx == 0 )
1271           fcString += QLatin1String( "  " );
1272         else
1273           fcString += QLatin1String( " ," );
1274         mJsonExporter.setSourceCrs( params.crs );
1275         mJsonExporter.setIncludeGeometry( false );
1276         mJsonExporter.setIncludeAttributes( !params.attributeIndexes.isEmpty() );
1277         mJsonExporter.setAttributes( params.attributeIndexes );
1278         fcString += createFeatureGeoJSON( feature, params, pkAttributes );
1279         fcString += QLatin1String( "\n" );
1280 
1281         response.write( fcString.toUtf8() );
1282       }
1283       else
1284       {
1285         QDomDocument gmlDoc;
1286         QDomElement featureElement;
1287         if ( format == QgsWfsParameters::Format::GML3 )
1288         {
1289           featureElement = createFeatureGML3( feature, gmlDoc, params, project, pkAttributes );
1290           gmlDoc.appendChild( featureElement );
1291         }
1292         else
1293         {
1294           featureElement = createFeatureGML2( feature, gmlDoc, params, project, pkAttributes );
1295           gmlDoc.appendChild( featureElement );
1296         }
1297         response.write( gmlDoc.toByteArray() );
1298       }
1299 
1300       // Stream partial content
1301       response.flush();
1302     }
1303 
endGetFeature(QgsServerResponse & response,QgsWfsParameters::Format format)1304     void endGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format )
1305     {
1306       QString fcString;
1307       if ( format == QgsWfsParameters::Format::GeoJSON )
1308       {
1309         fcString += QLatin1String( " ]\n" );
1310         fcString += QLatin1Char( '}' );
1311       }
1312       else
1313       {
1314         fcString = QStringLiteral( "</wfs:FeatureCollection>\n" );
1315       }
1316       response.write( fcString.toUtf8() );
1317     }
1318 
1319 
createFeatureGeoJSON(const QgsFeature & feature,const createFeatureParams & params,const QgsAttributeList & pkAttributes)1320     QString createFeatureGeoJSON( const QgsFeature &feature, const createFeatureParams &params, const QgsAttributeList &pkAttributes )
1321     {
1322       QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) );
1323       //QgsJsonExporter force transform geometry to EPSG:4326
1324       //and the RFC 7946 GeoJSON specification recommends limiting coordinate precision to 6
1325       //Q_UNUSED( prec )
1326 
1327       //copy feature so we can modify its geometry as required
1328       QgsFeature f( feature );
1329       QgsGeometry geom = feature.geometry();
1330       if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) )
1331       {
1332         mJsonExporter.setIncludeGeometry( true );
1333         if ( params.geometryName == QLatin1String( "EXTENT" ) )
1334         {
1335           QgsRectangle box = geom.boundingBox();
1336           f.setGeometry( QgsGeometry::fromRect( box ) );
1337         }
1338         else if ( params.geometryName == QLatin1String( "CENTROID" ) )
1339         {
1340           f.setGeometry( geom.centroid() );
1341         }
1342       }
1343 
1344       return mJsonExporter.exportFeature( f, QVariantMap(), id );
1345     }
1346 
1347 
createFeatureGML2(const QgsFeature & feature,QDomDocument & doc,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1348     QDomElement createFeatureGML2( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams &params, const QgsProject *project, const QgsAttributeList &pkAttributes )
1349     {
1350       //gml:FeatureMember
1351       QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ );
1352 
1353       //qgs:%TYPENAME%
1354       QDomElement typeNameElement = doc.createElement( "qgs:" + params.typeName /*qgs:%TYPENAME%*/ );
1355       QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) );
1356       typeNameElement.setAttribute( QStringLiteral( "fid" ), id );
1357       featureElement.appendChild( typeNameElement );
1358 
1359       //add geometry column (as gml)
1360       QgsGeometry geom = feature.geometry();
1361       if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) )
1362       {
1363         int prec = params.precision;
1364         QgsCoordinateReferenceSystem crs = params.crs;
1365         QgsCoordinateTransform mTransform( crs, params.outputCrs, project );
1366         try
1367         {
1368           QgsGeometry transformed = geom;
1369           if ( transformed.transform( mTransform ) == Qgis::GeometryOperationResult::Success )
1370           {
1371             geom = transformed;
1372             crs = params.outputCrs;
1373             if ( crs.isGeographic() && !params.crs.isGeographic() )
1374               prec = std::min( params.precision + 3, 6 );
1375           }
1376         }
1377         catch ( QgsCsException &cse )
1378         {
1379           Q_UNUSED( cse )
1380         }
1381 
1382         QDomElement geomElem = doc.createElement( QStringLiteral( "qgs:geometry" ) );
1383         QDomElement gmlElem;
1384         QgsGeometry cloneGeom( geom );
1385         if ( params.geometryName == QLatin1String( "EXTENT" ) )
1386         {
1387           cloneGeom = QgsGeometry::fromRect( geom.boundingBox() );
1388         }
1389         else if ( params.geometryName == QLatin1String( "CENTROID" ) )
1390         {
1391           cloneGeom = geom.centroid();
1392         }
1393         else if ( params.forceGeomToMulti && ! QgsWkbTypes::isMultiType( geom.wkbType() ) )
1394         {
1395           cloneGeom.convertToMultiType();
1396         }
1397         const QgsAbstractGeometry *abstractGeom = cloneGeom.constGet();
1398         if ( abstractGeom )
1399         {
1400           gmlElem = abstractGeom->asGml2( doc, prec, "http://www.opengis.net/gml" );
1401         }
1402 
1403         if ( !gmlElem.isNull() )
1404         {
1405           QgsRectangle box = geom.boundingBox();
1406           QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) );
1407           QDomElement boxElem = QgsOgcUtils::rectangleToGMLBox( &box, doc, prec );
1408 
1409           if ( crs.isValid() )
1410           {
1411             boxElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() );
1412             gmlElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() );
1413           }
1414 
1415           bbElem.appendChild( boxElem );
1416           typeNameElement.appendChild( bbElem );
1417 
1418           geomElem.appendChild( gmlElem );
1419           typeNameElement.appendChild( geomElem );
1420         }
1421       }
1422 
1423       //read all attribute values from the feature
1424       QgsAttributes featureAttributes = feature.attributes();
1425       QgsFields fields = feature.fields();
1426       for ( int i = 0; i < params.attributeIndexes.count(); ++i )
1427       {
1428         int idx = params.attributeIndexes[i];
1429         if ( idx >= fields.count() )
1430         {
1431           continue;
1432         }
1433         const QgsField field = fields.at( idx );
1434         const QgsEditorWidgetSetup setup = field.editorWidgetSetup();
1435         QString attributeName = field.name();
1436 
1437         QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) );
1438         QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) );
1439         if ( featureAttributes[idx].isNull() )
1440         {
1441           fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) );
1442         }
1443         fieldElem.appendChild( fieldText );
1444         typeNameElement.appendChild( fieldElem );
1445       }
1446 
1447       return featureElement;
1448     }
1449 
createFeatureGML3(const QgsFeature & feature,QDomDocument & doc,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1450     QDomElement createFeatureGML3( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams &params, const QgsProject *project, const QgsAttributeList &pkAttributes )
1451     {
1452       //gml:FeatureMember
1453       QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ );
1454 
1455       //qgs:%TYPENAME%
1456       QDomElement typeNameElement = doc.createElement( "qgs:" + params.typeName /*qgs:%TYPENAME%*/ );
1457       QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) );
1458       typeNameElement.setAttribute( QStringLiteral( "gml:id" ), id );
1459       featureElement.appendChild( typeNameElement );
1460 
1461       //add geometry column (as gml)
1462       QgsGeometry geom = feature.geometry();
1463       if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) )
1464       {
1465         int prec = params.precision;
1466         QgsCoordinateReferenceSystem crs = params.crs;
1467         QgsCoordinateTransform mTransform( crs, params.outputCrs, project );
1468         try
1469         {
1470           QgsGeometry transformed = geom;
1471           if ( transformed.transform( mTransform ) == Qgis::GeometryOperationResult::Success )
1472           {
1473             geom = transformed;
1474             crs = params.outputCrs;
1475             if ( crs.isGeographic() && !params.crs.isGeographic() )
1476               prec = std::min( params.precision + 3, 6 );
1477           }
1478         }
1479         catch ( QgsCsException &cse )
1480         {
1481           Q_UNUSED( cse )
1482         }
1483 
1484         QDomElement geomElem = doc.createElement( QStringLiteral( "qgs:geometry" ) );
1485         QDomElement gmlElem;
1486         QgsGeometry cloneGeom( geom );
1487         if ( params.geometryName == QLatin1String( "EXTENT" ) )
1488         {
1489           cloneGeom = QgsGeometry::fromRect( geom.boundingBox() );
1490         }
1491         else if ( params.geometryName == QLatin1String( "CENTROID" ) )
1492         {
1493           cloneGeom = geom.centroid();
1494         }
1495         else if ( params.forceGeomToMulti && ! QgsWkbTypes::isMultiType( geom.wkbType() ) )
1496         {
1497           cloneGeom.convertToMultiType();
1498         }
1499         const QgsAbstractGeometry *abstractGeom = cloneGeom.constGet();
1500         if ( abstractGeom )
1501         {
1502           gmlElem = abstractGeom->asGml3( doc, prec, "http://www.opengis.net/gml", params.hasAxisInverted ? QgsAbstractGeometry::AxisOrder::YX : QgsAbstractGeometry::AxisOrder::XY );
1503         }
1504 
1505         if ( !gmlElem.isNull() )
1506         {
1507           QgsRectangle box = geom.boundingBox();
1508           QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) );
1509           QDomElement boxElem = QgsOgcUtils::rectangleToGMLEnvelope( &box, doc, params.srsName, params.hasAxisInverted, prec );
1510 
1511           if ( crs.isValid() )
1512           {
1513             boxElem.setAttribute( QStringLiteral( "srsName" ), params.srsName );
1514             gmlElem.setAttribute( QStringLiteral( "srsName" ), params.srsName );
1515           }
1516 
1517           bbElem.appendChild( boxElem );
1518           typeNameElement.appendChild( bbElem );
1519 
1520           geomElem.appendChild( gmlElem );
1521           typeNameElement.appendChild( geomElem );
1522         }
1523       }
1524 
1525       //read all attribute values from the feature
1526       QgsAttributes featureAttributes = feature.attributes();
1527       QgsFields fields = feature.fields();
1528       for ( int i = 0; i < params.attributeIndexes.count(); ++i )
1529       {
1530         int idx = params.attributeIndexes[i];
1531         if ( idx >= fields.count() )
1532         {
1533           continue;
1534         }
1535 
1536         const QgsField field = fields.at( idx );
1537         const QgsEditorWidgetSetup setup = field.editorWidgetSetup();
1538 
1539         QString attributeName = field.name();
1540 
1541         QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) );
1542         QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) );
1543         if ( featureAttributes[idx].isNull() )
1544         {
1545           fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) );
1546         }
1547         fieldElem.appendChild( fieldText );
1548         typeNameElement.appendChild( fieldElem );
1549       }
1550 
1551       return featureElement;
1552     }
1553 
encodeValueToText(const QVariant & value,const QgsEditorWidgetSetup & setup)1554     QString encodeValueToText( const QVariant &value, const QgsEditorWidgetSetup &setup )
1555     {
1556       if ( value.isNull() )
1557         return QString();
1558 
1559       if ( setup.type() ==  QStringLiteral( "DateTime" ) )
1560       {
1561         QgsDateTimeFieldFormatter fieldFormatter;
1562         const QVariantMap config = setup.config();
1563         const QString fieldFormat = config.value( QStringLiteral( "field_format" ), fieldFormatter.defaultFormat( value.type() ) ).toString();
1564         QDateTime date = value.toDateTime();
1565 
1566         if ( date.isValid() )
1567         {
1568           return date.toString( fieldFormat );
1569         }
1570       }
1571       else if ( setup.type() ==  QStringLiteral( "Range" ) )
1572       {
1573         const QVariantMap config = setup.config();
1574         if ( config.contains( QStringLiteral( "Precision" ) ) )
1575         {
1576           // if precision is defined, use it
1577           bool ok;
1578           int precision( config[ QStringLiteral( "Precision" ) ].toInt( &ok ) );
1579           if ( ok )
1580             return QString::number( value.toDouble(), 'f', precision );
1581         }
1582       }
1583 
1584       switch ( value.type() )
1585       {
1586         case QVariant::Int:
1587         case QVariant::UInt:
1588         case QVariant::LongLong:
1589         case QVariant::ULongLong:
1590         case QVariant::Double:
1591           return value.toString();
1592 
1593         case QVariant::Bool:
1594           return value.toBool() ? QStringLiteral( "true" ) : QStringLiteral( "false" );
1595 
1596         case QVariant::StringList:
1597         case QVariant::List:
1598         case QVariant::Map:
1599         {
1600           QString v = QgsJsonUtils::encodeValue( value );
1601 
1602           //do we need CDATA
1603           if ( v.indexOf( '<' ) != -1 || v.indexOf( '&' ) != -1 )
1604             v.prepend( QStringLiteral( "<![CDATA[" ) ).append( QStringLiteral( "]]>" ) );
1605 
1606           return v;
1607         }
1608 
1609         default:
1610         case QVariant::String:
1611         {
1612           QString v = value.toString();
1613 
1614           //do we need CDATA
1615           if ( v.indexOf( '<' ) != -1 || v.indexOf( '&' ) != -1 )
1616             v.prepend( QStringLiteral( "<![CDATA[" ) ).append( QStringLiteral( "]]>" ) );
1617 
1618           return v;
1619         }
1620       }
1621     }
1622 
1623 
1624   } // namespace
1625 
1626 } // namespace QgsWfs
1627 
1628 
1629 
1630