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