1 /***************************************************************************
2                               qgswfsprovider.cpp
3                               -------------------
4   begin                : July 25, 2006
5   copyright            : (C) 2006 by Marco Hugentobler
6   email                : marco dot hugentobler at karto dot baug dot ethz dot ch
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "qgis.h"
19 #include "qgsfeature.h"
20 #include "qgsfields.h"
21 #include "qgsgeometry.h"
22 #include "qgscoordinatereferencesystem.h"
23 #include "qgslogger.h"
24 #include "qgsmessagelog.h"
25 #include "qgsogcutils.h"
26 #include "qgsoapifprovider.h"
27 #include "qgswfsdataitems.h"
28 #include "qgswfsconstants.h"
29 #include "qgswfsfeatureiterator.h"
30 #include "qgswfsprovider.h"
31 #include "qgswfscapabilities.h"
32 #include "qgswfsdescribefeaturetype.h"
33 #include "qgswfstransactionrequest.h"
34 #include "qgswfsshareddata.h"
35 #include "qgswfsutils.h"
36 #include "qgssettings.h"
37 
38 #include <QApplication>
39 #include <QDomDocument>
40 #include <QMessageBox>
41 #include <QDomNodeList>
42 #include <QNetworkRequest>
43 #include <QNetworkReply>
44 #include <QFile>
45 #include <QUrl>
46 #include <QWidget>
47 #include <QPair>
48 #include <QTimer>
49 
50 #include <cfloat>
51 
52 const QString QgsWFSProvider::WFS_PROVIDER_KEY = QStringLiteral( "WFS" );
53 const QString QgsWFSProvider::WFS_PROVIDER_DESCRIPTION = QStringLiteral( "WFS data provider" );
54 
55 
QgsWFSProvider(const QString & uri,const ProviderOptions & options,const QgsWfsCapabilities::Capabilities & caps)56 QgsWFSProvider::QgsWFSProvider( const QString &uri, const ProviderOptions &options, const QgsWfsCapabilities::Capabilities &caps )
57   : QgsVectorDataProvider( uri, options )
58   , mShared( new QgsWFSSharedData( uri ) )
59 {
60   mShared->mCaps = caps;
61   mShared->mServerMaxFeatures = caps.maxFeatures;
62 
63   connect( mShared.get(), &QgsWFSSharedData::raiseError, this, &QgsWFSProvider::pushErrorSlot );
64   connect( mShared.get(), &QgsWFSSharedData::extentUpdated, this, &QgsWFSProvider::fullExtentCalculated );
65 
66   if ( uri.isEmpty() )
67   {
68     mValid = false;
69     return;
70   }
71 
72   //create mSourceCrs from url if possible [WBC 111221] refactored from GetFeatureGET()
73   QString srsname = mShared->mURI.SRSName();
74   if ( !srsname.isEmpty() )
75   {
76     if ( srsname == QLatin1String( "EPSG:900913" ) )
77       mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( QStringLiteral( "EPSG:3857" ) );
78     else
79       mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( srsname );
80   }
81 
82   // Must be called first to establish the version, in case we are in auto-detection
83   if ( !getCapabilities() )
84   {
85     mValid = false;
86     return;
87   }
88 
89   if ( !mShared->mURI.sql().isEmpty() )
90   {
91     if ( !processSQL( mShared->mURI.sql(), mProcessSQLErrorMsg, mProcessSQLWarningMsg ) )
92     {
93       QgsMessageLog::logMessage( mProcessSQLErrorMsg, tr( "WFS" ) );
94       mValid = false;
95       return;
96     }
97     mSubsetString = mShared->mURI.sql();
98   }
99   else
100   {
101     mSubsetString = mShared->mURI.filter();
102 
103     //fetch attributes of layer and type of its geometry attribute
104     //WBC 111221: extracting geometry type here instead of getFeature allows successful
105     //layer creation even when no features are retrieved (due to, e.g., BBOX or FILTER)
106     if ( !describeFeatureType( mShared->mGeometryAttribute, mShared->mFields, mShared->mWKBType ) )
107     {
108       mValid = false;
109       return;
110     }
111     mThisTypenameFields = mShared->mFields;
112   }
113 
114   if ( !mShared->computeFilter( mProcessSQLErrorMsg ) )
115   {
116     QgsMessageLog::logMessage( mProcessSQLErrorMsg, tr( "WFS" ) );
117     mValid = false;
118     return;
119   }
120 
121   const auto GetGeometryTypeFromOneFeature = [&]()
122   {
123     const bool requestMadeFromMainThread = QThread::currentThread() == QApplication::instance()->thread();
124     auto downloader = qgis::make_unique<QgsFeatureDownloader>();
125     downloader->setImpl( qgis::make_unique<QgsWFSFeatureDownloaderImpl>( mShared.get(), downloader.get(), requestMadeFromMainThread ) );
126     connect( downloader.get(),
127              qgis::overload < QVector<QgsFeatureUniqueIdPair> >::of( &QgsFeatureDownloader::featureReceived ),
128              this, &QgsWFSProvider::featureReceivedAnalyzeOneFeature );
129     if ( requestMadeFromMainThread )
130     {
131       auto processEvents = []()
132       {
133         QApplication::instance()->processEvents();
134       };
135       connect( downloader.get(), &QgsFeatureDownloader::resumeMainThread,
136                this, processEvents );
137     }
138     downloader->run( false, /* serialize features */
139                      1 /* maxfeatures */ );
140   };
141 
142   //Failed to detect feature type from describeFeatureType -> get first feature from layer to detect type
143   if ( mShared->mWKBType == QgsWkbTypes::Unknown )
144   {
145     GetGeometryTypeFromOneFeature();
146 
147     // If we still didn't get the geometry type, and have a filter, temporarily
148     // disable the filter.
149     // See https://github.com/qgis/QGIS/issues/43950
150     if ( mShared->mWKBType == QgsWkbTypes::Unknown && !mSubsetString.isEmpty() )
151     {
152       const QString oldFilter = mShared->setWFSFilter( QString() );
153       GetGeometryTypeFromOneFeature();
154       mShared->setWFSFilter( oldFilter );
155     }
156   }
157 }
158 
~QgsWFSProvider()159 QgsWFSProvider::~QgsWFSProvider()
160 {
161   QgsDebugMsgLevel( QStringLiteral( "~QgsWFSProvider()" ), 4 );
162 }
163 
164 class QgsWFSProviderSQLFunctionValidator: public QgsSQLStatement::RecursiveVisitor
165 {
166   public:
167     QgsWFSProviderSQLFunctionValidator(
168       const QList<QgsWfsCapabilities::Function> &spatialPredicatesList,
169       const QList<QgsWfsCapabilities::Function> &functionList );
170 
hasError() const171     bool hasError() const { return mError; }
172 
errorMessage() const173     const QString &errorMessage() const { return mErrorMessage; }
174 
175   protected:
176     void visit( const QgsSQLStatement::NodeFunction &n ) override;
177 
178   private:
179     const QList<QgsWfsCapabilities::Function> &mSpatialPredicatesList;
180     const QList<QgsWfsCapabilities::Function> &mFunctionList;
181     bool mError;
182     QString mErrorMessage;
183 };
184 
QgsWFSProviderSQLFunctionValidator(const QList<QgsWfsCapabilities::Function> & spatialPredicatesList,const QList<QgsWfsCapabilities::Function> & functionList)185 QgsWFSProviderSQLFunctionValidator::QgsWFSProviderSQLFunctionValidator(
186   const QList<QgsWfsCapabilities::Function> &spatialPredicatesList,
187   const QList<QgsWfsCapabilities::Function> &functionList )
188   : mSpatialPredicatesList( spatialPredicatesList )
189   , mFunctionList( functionList )
190   , mError( false )
191 {
192 }
193 
visit(const QgsSQLStatement::NodeFunction & n)194 void QgsWFSProviderSQLFunctionValidator::visit( const QgsSQLStatement::NodeFunction &n )
195 {
196   if ( !mError )
197   {
198     bool foundMatch = false;
199     const auto constMSpatialPredicatesList = mSpatialPredicatesList;
200     for ( const QgsWfsCapabilities::Function &f : constMSpatialPredicatesList )
201     {
202       if ( n.name().compare( f.name, Qt::CaseInsensitive ) == 0 ||
203            QString( "ST_" + n.name() ).compare( f.name, Qt::CaseInsensitive ) == 0 )
204       {
205         foundMatch = true;
206       }
207     }
208     const auto constMFunctionList = mFunctionList;
209     for ( const QgsWfsCapabilities::Function &f : constMFunctionList )
210     {
211       if ( n.name().compare( f.name, Qt::CaseInsensitive ) == 0 )
212       {
213         foundMatch = true;
214       }
215     }
216     if ( !foundMatch )
217     {
218       mError = true;
219       mErrorMessage = QObject::tr( "Function '%1' is not declared by the WFS server" ).arg( n.name() );
220     }
221     QgsSQLStatement::RecursiveVisitor::visit( n );
222   }
223 }
224 
225 class QgsWFSProviderSQLColumnRefValidator: public QgsSQLStatement::RecursiveVisitor
226 {
227   public:
228     QgsWFSProviderSQLColumnRefValidator(
229       const QgsWfsCapabilities::Capabilities &caps,
230       const QString &defaultTypeName,
231       const QMap< QString, QString > &mapTypenameAliasToTypename,
232       const QMap < QString, QgsFields > &mapTypenameToFields,
233       const QMap < QString, QString > &mapTypenameToGeometryAttribute );
234 
hasError() const235     bool hasError() const { return mError; }
236 
errorMessage() const237     const QString &errorMessage() const { return mErrorMessage; }
238 
239   protected:
240     void visit( const QgsSQLStatement::NodeColumnRef &n ) override;
241 
242   private:
243     const QgsWfsCapabilities::Capabilities mCaps;
244     QString mDefaultTypeName;
245     const QMap< QString, QString > &mMapTableAliasToName;
246     const QMap < QString, QgsFields > &mMapTypenameToFields;
247     const QMap < QString, QString > &mMapTypenameToGeometryAttribute;
248     bool mError;
249     QString mErrorMessage;
250 };
251 
QgsWFSProviderSQLColumnRefValidator(const QgsWfsCapabilities::Capabilities & caps,const QString & defaultTypeName,const QMap<QString,QString> & mapTypenameAliasToTypename,const QMap<QString,QgsFields> & mapTypenameToFields,const QMap<QString,QString> & mapTypenameToGeometryAttribute)252 QgsWFSProviderSQLColumnRefValidator::QgsWFSProviderSQLColumnRefValidator(
253   const QgsWfsCapabilities::Capabilities &caps,
254   const QString &defaultTypeName,
255   const QMap< QString, QString > &mapTypenameAliasToTypename,
256   const QMap < QString, QgsFields > &mapTypenameToFields,
257   const QMap < QString, QString > &mapTypenameToGeometryAttribute )
258   : mCaps( caps )
259   , mDefaultTypeName( defaultTypeName )
260   , mMapTableAliasToName( mapTypenameAliasToTypename )
261   , mMapTypenameToFields( mapTypenameToFields )
262   , mMapTypenameToGeometryAttribute( mapTypenameToGeometryAttribute )
263   , mError( false )
264 {
265 }
266 
visit(const QgsSQLStatement::NodeColumnRef & n)267 void QgsWFSProviderSQLColumnRefValidator::visit( const QgsSQLStatement::NodeColumnRef &n )
268 {
269   if ( !mError && !n.star() )
270   {
271     QString typeName = mDefaultTypeName;
272     if ( !n.tableName().isEmpty() )
273     {
274       if ( mMapTableAliasToName.contains( n.tableName() ) )
275         typeName = mMapTableAliasToName[n.tableName()];
276       else if ( mMapTableAliasToName.contains( mCaps.addPrefixIfNeeded( n.tableName() ) ) )
277         typeName = mMapTableAliasToName[mCaps.addPrefixIfNeeded( n.tableName() )];
278       else
279       {
280         mError = true;
281         mErrorMessage = QObject::tr( "Column '%1' references a non existing table" ).
282                         arg( n.dump() );
283         return;
284       }
285     }
286 
287     QgsFields tableFields = mMapTypenameToFields[typeName];
288     int idx = tableFields.lookupField( n.name() );
289     if ( idx < 0 && mMapTypenameToGeometryAttribute[typeName] != n.name() )
290     {
291       mError = true;
292       mErrorMessage = QObject::tr( "Column '%1' references a non existing field" ).
293                       arg( n.dump() );
294       return;
295     }
296 
297     QgsSQLStatement::RecursiveVisitor::visit( n );
298   }
299 }
300 
301 
processSQL(const QString & sqlString,QString & errorMsg,QString & warningMsg)302 bool QgsWFSProvider::processSQL( const QString &sqlString, QString &errorMsg, QString &warningMsg )
303 {
304   QgsDebugMsgLevel( QStringLiteral( "Processing SQL: %1" ).arg( sqlString ), 4 );
305   errorMsg.clear();
306   warningMsg.clear();
307   QgsSQLStatement sql( sqlString );
308   if ( sql.hasParserError() )
309   {
310     QString parserErrorString( sql.parserErrorString() );
311     QStringList parts( parserErrorString.split( ',' ) );
312     parserErrorString.clear();
313     const auto constParts = parts;
314     for ( const QString &part : constParts )
315     {
316       QString newPart( part );
317       if ( part == QLatin1String( "syntax error" ) )
318         newPart = tr( "Syntax error." );
319       else if ( part == QLatin1String( " unexpected $end" ) )
320         newPart = tr( "Missing content at end of string." );
321       else if ( part.startsWith( QLatin1String( " unexpected " ) ) )
322         newPart = tr( "%1 is unexpected." ).arg( part.mid( QStringLiteral( " unexpected " ).size() ) );
323       else if ( part.startsWith( QLatin1String( " expecting " ) ) )
324         newPart = tr( "%1 is expected instead." ).arg( part.mid( QStringLiteral( " expecting " ).size() ) );
325       if ( !parserErrorString.isEmpty() )
326         parserErrorString += QLatin1Char( ' ' );
327       parserErrorString += newPart;
328     }
329     parserErrorString.replace( QLatin1String( " or " ), tr( "%1 or %2" ).arg( QString(), QString() ) );
330     parserErrorString.replace( QLatin1String( "COMMA" ), tr( "comma" ) );
331     parserErrorString.replace( QLatin1String( "IDENTIFIER" ), tr( "an identifier" ) );
332     errorMsg = tr( "SQL query is invalid: %1" ).arg( parserErrorString );
333     return false;
334   }
335   if ( !sql.doBasicValidationChecks( errorMsg ) )
336   {
337     errorMsg = tr( "SQL query is invalid: %1" ).arg( errorMsg );
338     return false;
339   }
340   if ( sql.rootNode()->nodeType() != QgsSQLStatement::ntSelect )
341   {
342     // Shouldn't happen
343     QgsDebugMsg( QStringLiteral( "SQL statement is not a SELECT. This should not happen" ) );
344     return false;
345   }
346   const QgsSQLStatement::NodeSelect *select = dynamic_cast<const QgsSQLStatement::NodeSelect *>( sql.rootNode() );
347   if ( !select )
348   {
349     // Makes Coverity happy, but cannot happen in practice
350     QgsDebugMsg( QStringLiteral( "should not happen" ) );
351     return false;
352   }
353   mShared->mDistinctSelect = select->distinct();
354 
355   QMap< QString, QString > mapTypenameAliasToTypename;
356   QMap< QString, QString > mapTypenameToTypenameAlias; // reverse map of the previous one
357   QList<QgsSQLStatement::NodeTableDef *> tables = select->tables();
358   QList< QString> typenameList;
359   bool severalTablesWithSameNameButDifferentPrefix = false;
360   QSet< QString > unprefixTypenames;
361   const auto constTables = tables;
362   for ( QgsSQLStatement::NodeTableDef *table : constTables )
363   {
364     QString prefixedTypename( mShared->mCaps.addPrefixIfNeeded( table->name() ) );
365     if ( prefixedTypename.isEmpty() )
366     {
367       if ( mShared->mCaps.setAmbiguousUnprefixedTypename.contains( table->name() ) )
368       {
369         errorMsg = tr( "Typename '%1' is ambiguous without prefix" ).arg( table->name() );
370       }
371       else
372       {
373         errorMsg = tr( "Typename '%1' is unknown" ).arg( table->name() );
374       }
375       return false;
376     }
377     typenameList << prefixedTypename;
378     if ( unprefixTypenames.contains( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) ) )
379     {
380       severalTablesWithSameNameButDifferentPrefix = true;
381     }
382     unprefixTypenames.insert( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) );
383     if ( table->alias().isEmpty() )
384     {
385       mapTypenameAliasToTypename[ prefixedTypename ] = prefixedTypename;
386       mapTypenameToTypenameAlias[ prefixedTypename ] = prefixedTypename;
387     }
388     else
389     {
390       mapTypenameAliasToTypename[ table->alias()] = prefixedTypename;
391       mapTypenameToTypenameAlias[ prefixedTypename ] = table->alias();
392     }
393   }
394 
395   QList<QgsSQLStatement::NodeJoin *> joins = select->joins();
396   const auto constJoins = joins;
397   for ( QgsSQLStatement::NodeJoin *join : constJoins )
398   {
399     QgsSQLStatement::NodeTableDef *table = join->tableDef();
400     QString prefixedTypename( mShared->mCaps.addPrefixIfNeeded( table->name() ) );
401     if ( prefixedTypename.isEmpty() )
402     {
403       if ( mShared->mCaps.setAmbiguousUnprefixedTypename.contains( table->name() ) )
404       {
405         errorMsg = tr( "Typename '%1' is ambiguous without prefix" ).arg( table->name() );
406       }
407       else
408       {
409         errorMsg = tr( "Typename '%1' is unknown" ).arg( table->name() );
410       }
411       return false;
412     }
413     typenameList << prefixedTypename;
414     if ( unprefixTypenames.contains( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) ) )
415     {
416       severalTablesWithSameNameButDifferentPrefix = true;
417     }
418     unprefixTypenames.insert( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) );
419     if ( table->alias().isEmpty() )
420     {
421       mapTypenameAliasToTypename[ prefixedTypename ] = prefixedTypename;
422       mapTypenameToTypenameAlias[ prefixedTypename ] = prefixedTypename;
423     }
424     else
425     {
426       mapTypenameAliasToTypename[ table->alias()] = prefixedTypename;
427       mapTypenameToTypenameAlias[ prefixedTypename ] = table->alias();
428     }
429   }
430 
431   if ( typenameList.size() > 1 && !mShared->mCaps.supportsJoins )
432   {
433     errorMsg = tr( "JOINs are not supported by this server" );
434     return false;
435   }
436 
437   if ( !typenameList.contains( mShared->mURI.typeName() ) )
438   {
439     errorMsg = tr( "FROM or JOIN clause should contain the table name '%1'" ).arg( mShared->mURI.typeName() );
440     return false;
441   }
442 
443   QString concatenatedTypenames;
444   for ( const QString &typeName : qgis::as_const( typenameList ) )
445   {
446     if ( !concatenatedTypenames.isEmpty() )
447       concatenatedTypenames += QLatin1Char( ',' );
448     concatenatedTypenames += typeName;
449   }
450 
451   QgsWFSDescribeFeatureType describeFeatureType( mShared->mURI );
452   if ( !describeFeatureType.requestFeatureType( mShared->mWFSVersion,
453        concatenatedTypenames, mShared->mCaps ) )
454   {
455     errorMsg = tr( "DescribeFeatureType failed for url %1: %2" ).
456                arg( dataSourceUri(), describeFeatureType.errorMessage() );
457     return false;
458   }
459 
460   QByteArray  response = describeFeatureType.response();
461 
462 
463   QDomDocument describeFeatureDocument;
464   errorMsg.clear();
465   if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) )
466   {
467     QgsDebugMsgLevel( response, 4 );
468     errorMsg = tr( "DescribeFeatureType failed for url %1: %2" ).
469                arg( dataSourceUri(), errorMsg );
470     return false;
471   }
472 
473   mShared->mLayerPropertiesList.clear();
474   QMap < QString, QgsFields > mapTypenameToFields;
475   QMap < QString, QString > mapTypenameToGeometryAttribute;
476   for ( const QString &typeName : qgis::as_const( typenameList ) )
477   {
478     QString geometryAttribute;
479     QgsFields fields;
480     QgsWkbTypes::Type geomType;
481     if ( !readAttributesFromSchema( describeFeatureDocument,
482                                     typeName,
483                                     geometryAttribute, fields, geomType, errorMsg ) )
484     {
485       errorMsg = tr( "Analysis of DescribeFeatureType response failed for url %1, typeName %2: %3" ).
486                  arg( dataSourceUri(), typeName, errorMsg );
487       return false;
488     }
489 
490     mapTypenameToFields[typeName] = fields;
491     mapTypenameToGeometryAttribute[typeName] = geometryAttribute;
492     if ( typeName == mShared->mURI.typeName() )
493     {
494       mShared->mGeometryAttribute = geometryAttribute;
495       mShared->mWKBType = geomType;
496       mThisTypenameFields = fields;
497     }
498 
499     QgsOgcUtils::LayerProperties layerProperties;
500     layerProperties.mName = typeName;
501     layerProperties.mGeometryAttribute = geometryAttribute;
502     if ( typeName == mShared->mURI.typeName() )
503       layerProperties.mSRSName = mShared->srsName();
504 
505     if ( typeName.contains( ':' ) )
506     {
507       layerProperties.mNamespaceURI = mShared->mCaps.getNamespaceForTypename( typeName );
508       layerProperties.mNamespacePrefix = QgsWFSUtils::nameSpacePrefix( typeName );
509     }
510 
511     mShared->mLayerPropertiesList << layerProperties;
512   }
513 
514   const QString &defaultTypeName = mShared->mURI.typeName();
515   QgsWFSProviderSQLColumnRefValidator oColumnValidator(
516     mShared->mCaps,
517     defaultTypeName,
518     mapTypenameAliasToTypename,
519     mapTypenameToFields,
520     mapTypenameToGeometryAttribute );
521   sql.acceptVisitor( oColumnValidator );
522   if ( oColumnValidator.hasError() )
523   {
524     errorMsg = oColumnValidator.errorMessage();
525     return false;
526   }
527 
528   if ( mShared->mURI.validateSqlFunctions() )
529   {
530     QgsWFSProviderSQLFunctionValidator oValidator( mShared->mCaps.spatialPredicatesList,
531         mShared->mCaps.functionList );
532     sql.acceptVisitor( oValidator );
533     if ( oValidator.hasError() )
534     {
535       errorMsg = oValidator.errorMessage();
536       return false;
537     }
538   }
539 
540   QList<QgsSQLStatement::NodeSelectedColumn *> columns = select->columns();
541   QMap< QString, QPair<QString, QString> > mapFieldNameToSrcLayerNameFieldName;
542   mShared->mFields.clear();
543   const auto constColumns = columns;
544   for ( QgsSQLStatement::NodeSelectedColumn *selectedcolumn : constColumns )
545   {
546     QgsSQLStatement::Node *column = selectedcolumn->column();
547     if ( column->nodeType() != QgsSQLStatement::ntColumnRef )
548     {
549       errorMsg = tr( "Column '%1' is not a direct reference to a table column." ).arg( column->dump() );
550       return false;
551     }
552     QgsSQLStatement::NodeColumnRef *columnRef = dynamic_cast<QgsSQLStatement::NodeColumnRef *>( column );
553     Q_ASSERT( columnRef );
554 
555     QString columnTableTypename = defaultTypeName;
556     if ( !columnRef->tableName().isEmpty() )
557     {
558       if ( mapTypenameAliasToTypename.contains( columnRef->tableName() ) )
559         columnTableTypename = mapTypenameAliasToTypename[columnRef->tableName()];
560       else
561         columnTableTypename = mShared->mCaps.addPrefixIfNeeded( columnRef->tableName() );
562     }
563 
564     if ( columnRef->star() )
565     {
566       // table.* syntax
567       if ( !columnRef->tableName().isEmpty() )
568       {
569         const QgsFields tableFields = mapTypenameToFields[columnTableTypename];
570         for ( int i = 0; i < tableFields.size(); i++ )
571         {
572           QgsField srcField = tableFields.at( i );
573           QString fieldName( srcField.name() );
574           // If several tables selected, prefix by table name
575           if ( typenameList.size() > 1 )
576           {
577             QString tablePrefix( mShared->mCaps.addPrefixIfNeeded( columnRef->tableName() ) );
578             if ( tablePrefix.isEmpty() ) // might be an alias
579               tablePrefix = columnRef->tableName();
580             if ( !severalTablesWithSameNameButDifferentPrefix )
581               tablePrefix = QgsWFSUtils::removeNamespacePrefix( tablePrefix );
582             fieldName = tablePrefix + "." + fieldName;
583           }
584           QgsField field( srcField );
585           field.setName( fieldName );
586           if ( mapFieldNameToSrcLayerNameFieldName.contains( fieldName ) )
587           {
588             errorMsg = tr( "Field '%1': a field with the same name already exists." ).arg( field.name() );
589             return false;
590           }
591 
592           mapFieldNameToSrcLayerNameFieldName[ field.name()] =
593             QPair<QString, QString>( columnTableTypename, srcField.name() );
594           mShared->mFields.append( field );
595         }
596       }
597       else
598       {
599         // * syntax
600         const auto constTypenameList = typenameList;
601         for ( const QString &typeName : constTypenameList )
602         {
603           const QgsFields tableFields = mapTypenameToFields[typeName];
604           for ( int i = 0; i < tableFields.size(); i++ )
605           {
606             QgsField srcField = tableFields.at( i );
607             QString fieldName( srcField.name() );
608             // If several tables selected, prefix by table name
609             if ( typenameList.size() > 1 )
610             {
611               QString tablePrefix( mapTypenameToTypenameAlias[typeName] );
612               if ( !severalTablesWithSameNameButDifferentPrefix )
613                 tablePrefix = QgsWFSUtils::removeNamespacePrefix( tablePrefix );
614               fieldName = tablePrefix + "." + fieldName;
615             }
616             QgsField field( srcField );
617             field.setName( fieldName );
618             mapFieldNameToSrcLayerNameFieldName[ field.name()] =
619               QPair<QString, QString>( typeName, srcField.name() );
620             mShared->mFields.append( field );
621           }
622         }
623       }
624     }
625     // Geometry field
626     else if ( mapTypenameToGeometryAttribute[columnTableTypename] == columnRef->name() )
627     {
628       if ( columnTableTypename != mShared->mURI.typeName() )
629       {
630         warningMsg = tr( "The geometry field of a typename that is not the main typename is ignored in the selected fields." );
631         QgsDebugMsg( warningMsg );
632       }
633     }
634     // Regular field
635     else
636     {
637       const QgsFields tableFields = mapTypenameToFields[columnTableTypename];
638       int idx = tableFields.lookupField( columnRef->name() );
639       if ( idx < 0 )
640       {
641         QgsDebugMsg( QStringLiteral( "Should not happen. Cannot find field for %1" ).arg( columnRef->name() ) );
642         continue;
643       }
644 
645       QString fieldName( columnRef->name() );
646       if ( !selectedcolumn->alias().isEmpty() )
647         fieldName = selectedcolumn->alias();
648       else if ( !columnRef->tableName().isEmpty() )
649       {
650         QString tablePrefix( mShared->mCaps.addPrefixIfNeeded( columnRef->tableName() ) );
651         if ( tablePrefix.isEmpty() ) // might be an alias
652           tablePrefix = columnRef->tableName();
653         if ( !severalTablesWithSameNameButDifferentPrefix )
654           tablePrefix = QgsWFSUtils::removeNamespacePrefix( tablePrefix );
655         fieldName = tablePrefix + "." + fieldName;
656       }
657       if ( mapFieldNameToSrcLayerNameFieldName.contains( fieldName ) )
658       {
659         errorMsg = tr( "Field '%1': a field with the same name already exists." ).arg( column->dump() );
660         return false;
661       }
662 
663       QgsField orig = tableFields.at( idx );
664       QgsField field( orig );
665       field.setName( fieldName );
666       mapFieldNameToSrcLayerNameFieldName[ field.name()] =
667         QPair<QString, QString>( columnTableTypename, orig.name() );
668       mShared->mFields.append( field );
669     }
670   }
671 
672   mShared->mMapFieldNameToSrcLayerNameFieldName = mapFieldNameToSrcLayerNameFieldName;
673 
674   return true;
675 }
676 
pushErrorSlot(const QString & errorMsg)677 void QgsWFSProvider::pushErrorSlot( const QString &errorMsg )
678 {
679   pushError( errorMsg );
680 }
681 
featureReceivedAnalyzeOneFeature(QVector<QgsFeatureUniqueIdPair> list)682 void QgsWFSProvider::featureReceivedAnalyzeOneFeature( QVector<QgsFeatureUniqueIdPair> list )
683 {
684   if ( list.size() != 0 )
685   {
686     QgsFeature feat = list[0].first;
687     QgsGeometry geometry = feat.geometry();
688     if ( !geometry.isNull() )
689     {
690       mShared->mWKBType = geometry.wkbType();
691 
692       // Fragile heuristics that helps for
693       // https://sampleservices.luciad.com/ogc/wfs/sampleswfs?REQUEST=GetFeature&SERVICE=WFS&TYPENAME=rivers&VERSION=1.1.0
694       // In case the geometry is a geometry collection, analyze its members to
695       // see if they are of the same type. If then, assume that all features
696       // will be similar, and report the proper MultiPoint/MultiLineString/
697       // MultiPolygon type instead.
698       if ( mShared->mWKBType == QgsWkbTypes::GeometryCollection )
699       {
700         auto geomColl = geometry.asGeometryCollection();
701         mShared->mWKBType = QgsWkbTypes::Unknown;
702         for ( const auto &geom : geomColl )
703         {
704           if ( mShared->mWKBType == QgsWkbTypes::Unknown )
705           {
706             mShared->mWKBType = geom.wkbType();
707           }
708           else if ( mShared->mWKBType != geom.wkbType() )
709           {
710             mShared->mWKBType = QgsWkbTypes::Unknown;
711             break;
712           }
713         }
714         if ( mShared->mWKBType != QgsWkbTypes::Unknown )
715         {
716           if ( mShared->mWKBType == QgsWkbTypes::Point )
717           {
718             QgsDebugMsg( QStringLiteral( "Layer of unknown type. First element is a GeometryCollection of Point. Advertizing optimistically as MultiPoint" ) );
719             mShared->mWKBType = QgsWkbTypes::MultiPoint;
720           }
721           else if ( mShared->mWKBType == QgsWkbTypes::LineString )
722           {
723             QgsDebugMsg( QStringLiteral( "Layer of unknown type. First element is a GeometryCollection of LineString. Advertizing optimistically as MultiLineString" ) );
724             mShared->mWKBType = QgsWkbTypes::MultiLineString;
725           }
726           else if ( mShared->mWKBType == QgsWkbTypes::Polygon )
727           {
728             QgsDebugMsg( QStringLiteral( "Layer of unknown type. First element is a GeometryCollection of Polygon. Advertizing optimistically as MultiPolygon" ) );
729             mShared->mWKBType = QgsWkbTypes::MultiPolygon;
730           }
731           else
732           {
733             mShared->mWKBType = QgsWkbTypes::Unknown;
734           }
735         }
736       }
737     }
738   }
739 }
740 
subsetString() const741 QString QgsWFSProvider::subsetString() const
742 {
743   return mSubsetString;
744 }
745 
setSubsetString(const QString & theSQL,bool updateFeatureCount)746 bool QgsWFSProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount )
747 {
748   Q_UNUSED( updateFeatureCount )
749 
750   QgsDebugMsgLevel( QStringLiteral( "theSql = '%1'" ).arg( theSQL ), 4 );
751 
752   if ( theSQL == mSubsetString )
753     return true;
754 
755   // Invalid and cancel current download before altering fields, etc...
756   // (crashes might happen if not done at the beginning)
757   mShared->invalidateCache();
758 
759   mSubsetString = theSQL;
760   clearMinMaxCache();
761 
762   // update URI
763   mShared->mFields = mThisTypenameFields;
764   mShared->mLayerPropertiesList.clear();
765   mShared->mMapFieldNameToSrcLayerNameFieldName.clear();
766   mShared->mDistinctSelect = false;
767   if ( theSQL.startsWith( QLatin1String( "SELECT " ), Qt::CaseInsensitive ) ||
768        theSQL.startsWith( QLatin1String( "SELECT\t" ), Qt::CaseInsensitive ) ||
769        theSQL.startsWith( QLatin1String( "SELECT\r" ), Qt::CaseInsensitive ) ||
770        theSQL.startsWith( QLatin1String( "SELECT\n" ), Qt::CaseInsensitive ) )
771   {
772     QString errorMsg, warningMsg;
773     if ( !processSQL( theSQL, errorMsg, warningMsg ) )
774     {
775       QgsMessageLog::logMessage( errorMsg, tr( "WFS" ) );
776       return false;
777     }
778     mShared->mURI.setSql( theSQL );
779     mShared->mURI.setFilter( QString() );
780   }
781   else
782   {
783     mShared->mURI.setSql( QString() );
784     mShared->mURI.setFilter( theSQL );
785   }
786 
787   setDataSourceUri( mShared->mURI.uri() );
788   QString errorMsg;
789   if ( !mShared->computeFilter( errorMsg ) )
790     QgsMessageLog::logMessage( errorMsg, tr( "WFS" ) );
791 
792   reloadData();
793 
794   return true;
795 }
796 
797 
featureSource() const798 QgsAbstractFeatureSource *QgsWFSProvider::featureSource() const
799 {
800   auto fs = new QgsBackgroundCachedFeatureSource( mShared );
801   /*connect( fs, SIGNAL( extentRequested( const QgsRectangle & ) ),
802            this, SLOT( extendExtent( const QgsRectangle & ) ) );*/
803   return fs;
804 }
805 
reloadProviderData()806 void QgsWFSProvider::reloadProviderData()
807 {
808   mShared->invalidateCache();
809 }
810 
geometryElement(const QgsGeometry & geometry,QDomDocument & transactionDoc)811 QDomElement QgsWFSProvider::geometryElement( const QgsGeometry &geometry, QDomDocument &transactionDoc )
812 {
813   QDomElement gmlElem;
814 
815   // Determine axis orientation and gml version
816   bool applyAxisInversion;
817   QgsOgcUtils::GMLVersion gmlVersion;
818 
819   if ( mShared->mWFSVersion.startsWith( QLatin1String( "1.1" ) ) )
820   {
821     // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2
822     if ( ! mShared->mServerPrefersCoordinatesForTransactions_1_1 )
823     {
824       gmlVersion = QgsOgcUtils::GML_3_1_0;
825     }
826     else
827     {
828       gmlVersion = QgsOgcUtils::GML_2_1_2;
829     }
830     // For servers like Geomedia and QGIS Server that advertise EPSG:XXXX in capabilities even in WFS 1.1 or 2.0
831     // cpabilities useEPSGColumnFormat is set.
832     // We follow GeoServer convention here which is to treat EPSG:4326 as lon/lat
833     applyAxisInversion = ( crs().hasAxisInverted() && ! mShared->mURI.ignoreAxisOrientation() && ! mShared->mCaps.useEPSGColumnFormat )
834                          || mShared->mURI.invertAxisOrientation();
835   }
836   else // 1.0
837   {
838     gmlVersion = QgsOgcUtils::GML_2_1_2;
839     applyAxisInversion = mShared->mURI.invertAxisOrientation();
840   }
841 
842   gmlElem = QgsOgcUtils::geometryToGML(
843               geometry,
844               transactionDoc,
845               gmlVersion,
846               mShared->srsName(),
847               applyAxisInversion,
848               QString()
849             );
850 
851   return gmlElem;
852 }
853 
wkbType() const854 QgsWkbTypes::Type QgsWFSProvider::wkbType() const
855 {
856   return mShared->mWKBType;
857 }
858 
featureCount() const859 long QgsWFSProvider::featureCount() const
860 {
861   return mShared->getFeatureCount();
862 }
863 
fields() const864 QgsFields QgsWFSProvider::fields() const
865 {
866   return mShared->mFields;
867 }
868 
geometryAttribute() const869 QString QgsWFSProvider::geometryAttribute() const
870 {
871   return mShared->mGeometryAttribute;
872 }
873 
crs() const874 QgsCoordinateReferenceSystem QgsWFSProvider::crs() const
875 {
876   return mShared->mSourceCrs;
877 }
878 
extent() const879 QgsRectangle QgsWFSProvider::extent() const
880 {
881   return mShared->consolidatedExtent();
882 }
883 
isValid() const884 bool QgsWFSProvider::isValid() const
885 {
886   return mValid;
887 }
888 
getFeatures(const QgsFeatureRequest & request) const889 QgsFeatureIterator QgsWFSProvider::getFeatures( const QgsFeatureRequest &request ) const
890 {
891   return QgsFeatureIterator( new QgsBackgroundCachedFeatureIterator( new QgsBackgroundCachedFeatureSource( mShared ), true, mShared, request ) );
892 }
893 
addFeatures(QgsFeatureList & flist,Flags flags)894 bool QgsWFSProvider::addFeatures( QgsFeatureList &flist, Flags flags )
895 {
896   //create <Transaction> xml
897   QDomDocument transactionDoc;
898   QDomElement transactionElem = createTransactionElement( transactionDoc );
899   transactionDoc.appendChild( transactionElem );
900 
901   //find out typename from uri and strip namespace prefix
902   QString tname = mShared->mURI.typeName();
903   if ( tname.isNull() )
904   {
905     return false;
906   }
907   tname = QgsWFSUtils::removeNamespacePrefix( tname );
908 
909   //Add the features
910   QgsFeatureList::iterator featureIt = flist.begin();
911   for ( ; featureIt != flist.end(); ++featureIt )
912   {
913     //Insert element
914     QDomElement insertElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Insert" ) );
915     transactionElem.appendChild( insertElem );
916 
917     QDomElement featureElem = transactionDoc.createElementNS( mApplicationNamespace, tname );
918 
919     QgsAttributes featureAttributes = featureIt->attributes();
920     int nAttrs = featureAttributes.size();
921     for ( int i = 0; i < nAttrs; ++i )
922     {
923       const QVariant &value = featureAttributes.at( i );
924       if ( value.isValid() && !value.isNull() )
925       {
926         QDomElement fieldElem = transactionDoc.createElementNS( mApplicationNamespace, mShared->mFields.at( i ).name() );
927         QDomText fieldText = transactionDoc.createTextNode( convertToXML( value ) );
928         fieldElem.appendChild( fieldText );
929         featureElem.appendChild( fieldElem );
930       }
931     }
932 
933     //add geometry column (as gml)
934     QgsGeometry geometry = featureIt->geometry();
935     if ( !geometry.isNull() )
936     {
937       QDomElement geomElem = transactionDoc.createElementNS( mApplicationNamespace, mShared->mGeometryAttribute );
938       QgsGeometry the_geom( geometry );
939       // convert to multi if the layer geom type is multi and the geom is not
940       if ( QgsWkbTypes::isMultiType( this->wkbType() ) && ! the_geom.isMultipart() )
941       {
942         the_geom.convertToMultiType();
943       }
944 
945       const QDomElement gmlElem { geometryElement( the_geom, transactionDoc ) };
946       if ( ! gmlElem.isNull() )
947       {
948         geomElem.appendChild( gmlElem );
949         featureElem.appendChild( geomElem );
950       }
951     }
952 
953     insertElem.appendChild( featureElem );
954   }
955 
956   QDomDocument serverResponse;
957   bool success = sendTransactionDocument( transactionDoc, serverResponse );
958   if ( !success )
959   {
960     return false;
961   }
962 
963   if ( transactionSuccess( serverResponse ) )
964   {
965     //transaction successful. Add the features to the cache
966     QStringList idList = insertedFeatureIds( serverResponse );
967     /* Fix issue with GeoServer and shapefile feature stores when no real
968        feature id are returned but new0 returned instead of the featureId*/
969     const auto constIdList = idList;
970     for ( const QString &v : constIdList )
971     {
972       if ( v.startsWith( QLatin1String( "new" ) ) )
973       {
974         reloadData();
975         return true;
976       }
977     }
978     QStringList::const_iterator idIt = idList.constBegin();
979     featureIt = flist.begin();
980 
981     QVector<QgsFeatureUniqueIdPair> serializedFeatureList;
982     for ( ; idIt != idList.constEnd() && featureIt != flist.end(); ++idIt, ++featureIt )
983     {
984       serializedFeatureList.push_back( QgsFeatureUniqueIdPair( *featureIt, *idIt ) );
985     }
986     mShared->serializeFeatures( serializedFeatureList );
987 
988     if ( !( flags & QgsFeatureSink::FastInsert ) )
989     {
990       // And now set the feature id from the one got from the database
991       QMap< QString, QgsFeatureId > map;
992       for ( int idx = 0; idx < serializedFeatureList.size(); idx++ )
993         map[ serializedFeatureList[idx].second ] = serializedFeatureList[idx].first.id();
994 
995       idIt = idList.constBegin();
996       featureIt = flist.begin();
997       for ( ; idIt != idList.constEnd() && featureIt != flist.end(); ++idIt, ++featureIt )
998       {
999         if ( map.find( *idIt ) != map.end() )
1000           featureIt->setId( map[*idIt] );
1001       }
1002     }
1003 
1004     return true;
1005   }
1006   else
1007   {
1008     handleException( serverResponse );
1009     return false;
1010   }
1011 }
1012 
deleteFeatures(const QgsFeatureIds & id)1013 bool QgsWFSProvider::deleteFeatures( const QgsFeatureIds &id )
1014 {
1015   if ( id.size() < 1 )
1016   {
1017     return true;
1018   }
1019 
1020   //find out typename from uri and strip namespace prefix
1021   QString tname = mShared->mURI.typeName();
1022   if ( tname.isNull() )
1023   {
1024     return false;
1025   }
1026 
1027   //create <Transaction> xml
1028   QDomDocument transactionDoc;
1029   QDomElement transactionElem = createTransactionElement( transactionDoc );
1030   transactionDoc.appendChild( transactionElem );
1031   //delete element
1032   QDomElement deleteElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Delete" ) );
1033   deleteElem.setAttribute( QStringLiteral( "typeName" ), tname );
1034   QDomElement filterElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "Filter" ) );
1035 
1036 
1037   QgsFeatureIds::const_iterator idIt = id.constBegin();
1038   for ( ; idIt != id.constEnd(); ++idIt )
1039   {
1040     //find out feature id
1041     QString gmlid = mShared->findUniqueId( *idIt );
1042     if ( gmlid.isEmpty() )
1043     {
1044       QgsDebugMsg( QStringLiteral( "Cannot identify feature of id %1" ).arg( *idIt ) );
1045       continue;
1046     }
1047     QDomElement featureIdElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1048     featureIdElem.setAttribute( QStringLiteral( "fid" ), gmlid );
1049     filterElem.appendChild( featureIdElem );
1050   }
1051 
1052   deleteElem.appendChild( filterElem );
1053   transactionElem.appendChild( deleteElem );
1054 
1055   QDomDocument serverResponse;
1056   bool success = sendTransactionDocument( transactionDoc, serverResponse );
1057   if ( !success )
1058   {
1059     return false;
1060   }
1061 
1062   if ( transactionSuccess( serverResponse ) )
1063   {
1064     mShared->deleteFeatures( id );
1065     return true;
1066   }
1067   else
1068   {
1069     handleException( serverResponse );
1070     return false;
1071   }
1072 }
1073 
changeGeometryValues(const QgsGeometryMap & geometry_map)1074 bool QgsWFSProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
1075 {
1076   //find out typename from uri
1077   QString tname = mShared->mURI.typeName();
1078   if ( tname.isNull() )
1079   {
1080     return false;
1081   }
1082 
1083   QString namespacePrefix = QgsWFSUtils::nameSpacePrefix( tname );
1084   if ( !namespacePrefix.isEmpty() )
1085   {
1086     namespacePrefix += ':';
1087   }
1088 
1089   //create <Transaction> xml
1090   QDomDocument transactionDoc;
1091   QDomElement transactionElem = createTransactionElement( transactionDoc );
1092   transactionDoc.appendChild( transactionElem );
1093 
1094   QgsGeometryMap::const_iterator geomIt = geometry_map.constBegin();
1095   for ( ; geomIt != geometry_map.constEnd(); ++geomIt )
1096   {
1097     QString gmlid = mShared->findUniqueId( geomIt.key() );
1098     if ( gmlid.isEmpty() )
1099     {
1100       QgsDebugMsg( QStringLiteral( "Cannot identify feature of id %1" ).arg( geomIt.key() ) );
1101       continue;
1102     }
1103     QDomElement updateElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Update" ) );
1104     updateElem.setAttribute( QStringLiteral( "typeName" ), tname );
1105     //Property
1106     QDomElement propertyElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Property" ) );
1107     QDomElement nameElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Name" ) );
1108     QDomText nameText = transactionDoc.createTextNode( namespacePrefix + mShared->mGeometryAttribute );
1109     nameElem.appendChild( nameText );
1110     propertyElem.appendChild( nameElem );
1111     QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) );
1112 
1113     valueElem.appendChild( geometryElement( geomIt.value(), transactionDoc ) );
1114 
1115     propertyElem.appendChild( valueElem );
1116     updateElem.appendChild( propertyElem );
1117 
1118     //filter
1119     QDomElement filterElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "Filter" ) );
1120     QDomElement featureIdElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1121     featureIdElem.setAttribute( QStringLiteral( "fid" ), gmlid );
1122     filterElem.appendChild( featureIdElem );
1123     updateElem.appendChild( filterElem );
1124 
1125     transactionElem.appendChild( updateElem );
1126   }
1127 
1128   QDomDocument serverResponse;
1129   bool success = sendTransactionDocument( transactionDoc, serverResponse );
1130   if ( !success )
1131   {
1132     return false;
1133   }
1134 
1135   if ( transactionSuccess( serverResponse ) )
1136   {
1137     mShared->changeGeometryValues( geometry_map );
1138     return true;
1139   }
1140   else
1141   {
1142     handleException( serverResponse );
1143     return false;
1144   }
1145 }
1146 
convertToXML(const QVariant & value)1147 QString QgsWFSProvider::convertToXML( const QVariant &value )
1148 {
1149   QString valueStr( value.toString() );
1150   if ( value.type() == QVariant::DateTime )
1151   {
1152     QDateTime dt = value.toDateTime().toUTC();
1153     if ( !dt.isNull() )
1154     {
1155       valueStr = dt.toString( QStringLiteral( "yyyy-MM-ddThh:mm:ss.zzzZ" ) );
1156     }
1157     else
1158     {
1159       valueStr = QString();
1160     }
1161   }
1162   return valueStr;
1163 }
1164 
changeAttributeValues(const QgsChangedAttributesMap & attr_map)1165 bool QgsWFSProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
1166 {
1167   //find out typename from uri
1168   QString tname = mShared->mURI.typeName();
1169   if ( tname.isNull() )
1170   {
1171     return false;
1172   }
1173 
1174   QString namespacePrefix = QgsWFSUtils::nameSpacePrefix( tname );
1175   if ( !namespacePrefix.isEmpty() )
1176   {
1177     namespacePrefix += ':';
1178   }
1179 
1180   //create <Transaction> xml
1181   QDomDocument transactionDoc;
1182   QDomElement transactionElem = createTransactionElement( transactionDoc );
1183   transactionDoc.appendChild( transactionElem );
1184 
1185   QgsChangedAttributesMap::const_iterator attIt = attr_map.constBegin();
1186   for ( ; attIt != attr_map.constEnd(); ++attIt )
1187   {
1188     QString gmlid = mShared->findUniqueId( attIt.key() );
1189     if ( gmlid.isEmpty() )
1190     {
1191       QgsDebugMsg( QStringLiteral( "Cannot identify feature of id %1" ).arg( attIt.key() ) );
1192       continue;
1193     }
1194 
1195     QDomElement updateElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Update" ) );
1196     updateElem.setAttribute( QStringLiteral( "typeName" ), tname );
1197 
1198     QgsAttributeMap::const_iterator attMapIt = attIt.value().constBegin();
1199     for ( ; attMapIt != attIt.value().constEnd(); ++attMapIt )
1200     {
1201       QString fieldName = mShared->mFields.at( attMapIt.key() ).name();
1202       QDomElement propertyElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Property" ) );
1203 
1204       QDomElement nameElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Name" ) );
1205       QDomText nameText = transactionDoc.createTextNode( namespacePrefix + fieldName );
1206       nameElem.appendChild( nameText );
1207       propertyElem.appendChild( nameElem );
1208 
1209       QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) );
1210 
1211       if ( attMapIt.value().isValid() && !attMapIt.value().isNull() )
1212       {
1213         // WFS does not support :nil='true'
1214         // if value is NULL, do not add value element
1215         QDomText valueText = transactionDoc.createTextNode( convertToXML( attMapIt.value() ) );
1216         valueElem.appendChild( valueText );
1217         propertyElem.appendChild( valueElem );
1218       }
1219 
1220       updateElem.appendChild( propertyElem );
1221     }
1222 
1223     //Filter
1224     QDomElement filterElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "Filter" ) );
1225     QDomElement featureIdElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1226     featureIdElem.setAttribute( QStringLiteral( "fid" ), gmlid );
1227     filterElem.appendChild( featureIdElem );
1228     updateElem.appendChild( filterElem );
1229 
1230     transactionElem.appendChild( updateElem );
1231   }
1232 
1233   QDomDocument serverResponse;
1234   bool success = sendTransactionDocument( transactionDoc, serverResponse );
1235   if ( !success )
1236   {
1237     return false;
1238   }
1239 
1240   if ( transactionSuccess( serverResponse ) )
1241   {
1242     mShared->changeAttributeValues( attr_map );
1243     return true;
1244   }
1245   else
1246   {
1247     handleException( serverResponse );
1248     return false;
1249   }
1250 }
1251 
metadata() const1252 QVariantMap QgsWFSProvider::metadata() const
1253 {
1254   QVariantMap result;
1255   result[QStringLiteral( "MaxFeatures" )] = mShared->mCaps.maxFeatures;
1256   result[QStringLiteral( "SupportsPaging" )] = mShared->mCaps.supportsPaging;
1257   result[QStringLiteral( "SupportsJoins" )] = mShared->mCaps.supportsJoins;
1258   return result;
1259 }
1260 
translateMetadataKey(const QString & mdKey) const1261 QString QgsWFSProvider::translateMetadataKey( const QString &mdKey ) const
1262 {
1263   if ( mdKey == QLatin1String( "MaxFeatures" ) )
1264   {
1265     return tr( "Max Features" );
1266   }
1267   else if ( mdKey == QLatin1String( "SupportsPaging" ) )
1268   {
1269     return tr( "Supports Paging" );
1270   }
1271   else if ( mdKey == QLatin1String( "SupportsJoins" ) )
1272   {
1273     return tr( "Supports Joins" );
1274   }
1275   else
1276   {
1277     return mdKey;
1278   }
1279 };
1280 
translateMetadataValue(const QString & mdKey,const QVariant & value) const1281 QString QgsWFSProvider::translateMetadataValue( const QString &mdKey, const QVariant &value ) const
1282 {
1283   if ( mdKey == QLatin1String( "MaxFeatures" ) )
1284   {
1285     return value.toInt() == 0 ? tr( "not provided" ) : value.toString();
1286   }
1287   else if ( mdKey == QLatin1String( "SupportsPaging" ) || mdKey == QLatin1String( "SupportsJoins" ) )
1288   {
1289     return value.toBool() ? tr( "supported" ) : tr( "unsupported" );
1290   }
1291   else
1292   {
1293     return value.toString();
1294   }
1295 }
1296 
empty() const1297 bool QgsWFSProvider::empty() const
1298 {
1299   QgsFeature f;
1300   QgsFeatureRequest request;
1301   request.setNoAttributes();
1302   request.setFlags( QgsFeatureRequest::NoGeometry );
1303 
1304   // Whoops, the WFS provider returns an empty iterator when we are using
1305   // a setLimit call in combination with a subsetString.
1306   // Remove this method (and default to the QgsVectorDataProvider one)
1307   // once this is fixed
1308 #if 0
1309   request.setLimit( 1 );
1310 #endif
1311   return !getFeatures( request ).nextFeature( f );
1312 
1313 };
1314 
describeFeatureType(QString & geometryAttribute,QgsFields & fields,QgsWkbTypes::Type & geomType)1315 bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields &fields, QgsWkbTypes::Type &geomType )
1316 {
1317   fields.clear();
1318 
1319   QgsWFSDescribeFeatureType describeFeatureType( mShared->mURI );
1320 
1321   if ( !describeFeatureType.requestFeatureType( mShared->mWFSVersion,
1322        mShared->mURI.typeName(), mShared->mCaps ) )
1323   {
1324     QgsMessageLog::logMessage( tr( "DescribeFeatureType network request failed for url %1: %2" ).
1325                                arg( dataSourceUri(), describeFeatureType.errorMessage() ), tr( "WFS" ) );
1326     return false;
1327   }
1328 
1329   QByteArray response = describeFeatureType.response();
1330 
1331   QgsDebugMsgLevel( response, 4 );
1332 
1333   QDomDocument describeFeatureDocument;
1334   QString errorMsg;
1335   if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) )
1336   {
1337     QgsDebugMsgLevel( response, 4 );
1338     QgsMessageLog::logMessage( tr( "DescribeFeatureType XML parse failed for url %1: %2" ).
1339                                arg( dataSourceUri(), errorMsg ), tr( "WFS" ) );
1340     return false;
1341   }
1342 
1343   if ( !readAttributesFromSchema( describeFeatureDocument,
1344                                   mShared->mURI.typeName(),
1345                                   geometryAttribute, fields, geomType, errorMsg ) )
1346   {
1347     QgsDebugMsgLevel( response, 4 );
1348     QgsMessageLog::logMessage( tr( "Analysis of DescribeFeatureType response failed for url %1: %2" ).
1349                                arg( dataSourceUri(), errorMsg ), tr( "WFS" ) );
1350     return false;
1351   }
1352 
1353   return true;
1354 }
1355 
readAttributesFromSchema(QDomDocument & schemaDoc,const QString & prefixedTypename,QString & geometryAttribute,QgsFields & fields,QgsWkbTypes::Type & geomType,QString & errorMsg)1356 bool QgsWFSProvider::readAttributesFromSchema( QDomDocument &schemaDoc,
1357     const QString &prefixedTypename,
1358     QString &geometryAttribute,
1359     QgsFields &fields,
1360     QgsWkbTypes::Type &geomType,
1361     QString &errorMsg )
1362 {
1363   //get the <schema> root element
1364   QDomNodeList schemaNodeList = schemaDoc.elementsByTagNameNS( QgsWFSConstants::XMLSCHEMA_NAMESPACE, QStringLiteral( "schema" ) );
1365   if ( schemaNodeList.length() < 1 )
1366   {
1367     errorMsg = tr( "Cannot find schema root element" );
1368     return false;
1369   }
1370   QDomElement schemaElement = schemaNodeList.at( 0 ).toElement();
1371   mApplicationNamespace = schemaElement.attribute( QStringLiteral( "targetNamespace" ) );
1372 
1373   // Remove the namespace on the typename
1374   QString unprefixedTypename = prefixedTypename;
1375   if ( unprefixedTypename.contains( ':' ) )
1376   {
1377     unprefixedTypename = unprefixedTypename.section( ':', 1 );
1378   }
1379 
1380   // Find the element whose name is the typename that interests us, and
1381   // collect the correspond type.
1382   QDomElement elementElement = schemaElement.firstChildElement( QStringLiteral( "element" ) );
1383   QString elementTypeString;
1384   QDomElement complexTypeElement;
1385   while ( !elementElement.isNull() )
1386   {
1387     QString name = elementElement.attribute( QStringLiteral( "name" ) );
1388     if ( name == unprefixedTypename )
1389     {
1390       elementTypeString = elementElement.attribute( QStringLiteral( "type" ) );
1391       if ( elementTypeString.isEmpty() )
1392       {
1393         // e.g http://afnemers.ruimtelijkeplannen.nl/afnemers2012/services?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=app:Bouwvlak
1394         complexTypeElement = elementElement.firstChildElement( QStringLiteral( "complexType" ) );
1395       }
1396       break;
1397     }
1398     elementElement = elementElement.nextSiblingElement( QStringLiteral( "element" ) );
1399   }
1400   // Try to get a complex type whose name contains the unprefixed typename
1401   if ( elementTypeString.isEmpty() && complexTypeElement.isNull() )
1402   {
1403     const QDomNodeList complexElements = schemaElement.elementsByTagName( QStringLiteral( "complexType" ) );
1404     for ( int i = 0; i < complexElements.size(); i++ )
1405     {
1406       if ( complexElements.at( i ).toElement().attribute( QStringLiteral( "name" ) ).contains( unprefixedTypename ) )
1407       {
1408         complexTypeElement = complexElements.at( i ).toElement();
1409         break;
1410       }
1411     }
1412   }
1413   // Give up :(
1414   if ( elementTypeString.isEmpty() && complexTypeElement.isNull() )
1415   {
1416     // "http://demo.deegree.org/inspire-workspace/services/wfs?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=ad:Address"
1417     QDomElement iter = schemaElement.firstChildElement();
1418     bool onlyIncludeOrImport = true;
1419     bool foundImport = false;
1420     int countInclude = 0;
1421     QDomElement includeElement;
1422     while ( !iter.isNull() )
1423     {
1424       if ( iter.tagName() == QLatin1String( "import" ) )
1425         foundImport = true;
1426       else if ( iter.tagName() == QLatin1String( "include" ) )
1427       {
1428         countInclude++;
1429         if ( countInclude == 1 )
1430         {
1431           includeElement = iter;
1432         }
1433       }
1434       else
1435       {
1436         onlyIncludeOrImport = false;
1437         break;
1438       }
1439       iter = iter.nextSiblingElement();
1440     }
1441     if ( foundImport && onlyIncludeOrImport )
1442     {
1443       errorMsg = tr( "It is probably a schema for Complex Features." );
1444     }
1445     // e.g http://services.cuzk.cz/wfs/inspire-CP-wfs.asp?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType
1446     // which has a single  <include schemaLocation="http://inspire.ec.europa.eu/schemas/cp/4.0/CadastralParcels.xsd"/>
1447     // In that case, follow the link.
1448     else if ( !foundImport && countInclude == 1 )
1449     {
1450       QString schemaLocation =
1451         includeElement.attribute( QStringLiteral( "schemaLocation" ) );
1452       QgsDebugMsgLevel( QStringLiteral( "DescribeFeatureType response redirects to: %1" ).arg( schemaLocation ), 4 );
1453 
1454       QgsWFSDescribeFeatureType describeFeatureType( mShared->mURI );
1455       if ( !describeFeatureType.sendGET( schemaLocation, QString(), true, false ) )
1456       {
1457         errorMsg = tr( "Cannot find schema indicated in DescribeFeatureType response." );
1458         QgsMessageLog::logMessage( tr( "DescribeFeatureType network request failed for url %1: %2" ).
1459                                    arg( schemaLocation, describeFeatureType.errorMessage() ), tr( "WFS" ) );
1460         return false;
1461       }
1462 
1463       QByteArray response = describeFeatureType.response();
1464       QDomDocument describeFeatureDocument;
1465       if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) )
1466       {
1467         QgsDebugMsgLevel( response, 4 );
1468         errorMsg = tr( "DescribeFeatureType XML parse failed for url %1: %2" ).
1469                    arg( schemaLocation, errorMsg );
1470       }
1471 
1472       return readAttributesFromSchema( describeFeatureDocument,
1473                                        prefixedTypename,
1474                                        geometryAttribute,
1475                                        fields,
1476                                        geomType,
1477                                        errorMsg );
1478 
1479     }
1480     else
1481     {
1482       errorMsg = tr( "Cannot find element '%1'" ).arg( unprefixedTypename );
1483     }
1484     return false;
1485   }
1486 
1487   //remove the namespace on type
1488   if ( elementTypeString.contains( ':' ) )
1489   {
1490     elementTypeString = elementTypeString.section( ':', 1 );
1491   }
1492 
1493   if ( complexTypeElement.isNull() )
1494   {
1495     //the <complexType> element corresponding to the feature type
1496     complexTypeElement = schemaElement.firstChildElement( QStringLiteral( "complexType" ) );
1497     while ( !complexTypeElement.isNull() )
1498     {
1499       QString name = complexTypeElement.attribute( QStringLiteral( "name" ) );
1500       if ( name == elementTypeString )
1501       {
1502         break;
1503       }
1504       complexTypeElement = complexTypeElement.nextSiblingElement( QStringLiteral( "complexType" ) );
1505     }
1506     if ( complexTypeElement.isNull() )
1507     {
1508       errorMsg = tr( "Cannot find ComplexType element '%1'" ).arg( elementTypeString );
1509       return false;
1510     }
1511   }
1512 
1513   //we have the relevant <complexType> element. Now find out the geometry and the thematic attributes
1514   QDomNodeList attributeNodeList = complexTypeElement.elementsByTagNameNS( QgsWFSConstants::XMLSCHEMA_NAMESPACE, QStringLiteral( "element" ) );
1515   if ( attributeNodeList.size() < 1 )
1516   {
1517     errorMsg = tr( "Cannot find attribute elements" );
1518     return false;
1519   }
1520 
1521   bool foundGeometryAttribute = false;
1522 
1523   for ( int i = 0; i < attributeNodeList.size(); ++i )
1524   {
1525     QDomElement attributeElement = attributeNodeList.at( i ).toElement();
1526 
1527     //attribute name
1528     QString name = attributeElement.attribute( QStringLiteral( "name" ) );
1529     // Some servers like http://ogi.state.ok.us/geoserver/wfs on layer ogi:doq_centroids
1530     // return attribute names padded with spaces. See https://github.com/qgis/QGIS/issues/13486
1531     // I'm not completely sure how legal this
1532     // is but this validates with Xerces 3.1, and its schema analyzer does also the trimming.
1533     name = name.trimmed();
1534 
1535     //attribute type
1536     QString type = attributeElement.attribute( QStringLiteral( "type" ) );
1537     if ( type.isEmpty() )
1538     {
1539       QDomElement extension = attributeElement.firstChildElement( QStringLiteral( "complexType" ) ).
1540                               firstChildElement( QStringLiteral( "simpleContent" ) ).firstChildElement( QStringLiteral( "extension" ) );
1541       if ( !extension.isNull() )
1542       {
1543         type = extension.attribute( QStringLiteral( "base" ) );
1544       }
1545     }
1546 
1547     // attribute ref
1548     QString ref = attributeElement.attribute( QStringLiteral( "ref" ) );
1549 
1550     QRegExp gmlPT( "gml:(.*)PropertyType" );
1551     QRegExp gmlRefProperty( "gml:(.*)Property" );
1552 
1553     // gmgml: is Geomedia Web Server
1554     if ( ! foundGeometryAttribute && type == QLatin1String( "gmgml:Polygon_Surface_MultiSurface_CompositeSurfacePropertyType" ) )
1555     {
1556       foundGeometryAttribute = true;
1557       geometryAttribute = name;
1558       geomType = QgsWkbTypes::MultiPolygon;
1559     }
1560     else if ( ! foundGeometryAttribute && type == QLatin1String( "gmgml:LineString_Curve_MultiCurve_CompositeCurvePropertyType" ) )
1561     {
1562       foundGeometryAttribute = true;
1563       geometryAttribute = name;
1564       geomType = QgsWkbTypes::MultiLineString;
1565     }
1566     // such as http://go.geozug.ch/Zug_WFS_Baumkataster/service.svc/get
1567     else if ( type == QLatin1String( "gmgml:Point_MultiPointPropertyType" ) )
1568     {
1569       foundGeometryAttribute = true;
1570       geometryAttribute = name;
1571       geomType = QgsWkbTypes::MultiPoint;
1572     }
1573     //is it a geometry attribute?
1574     // the GeometryAssociationType has been seen in #11785
1575     else if ( ! foundGeometryAttribute && ( type.indexOf( gmlPT ) == 0 || type == QLatin1String( "gml:GeometryAssociationType" ) ) )
1576     {
1577       foundGeometryAttribute = true;
1578       geometryAttribute = name;
1579       // We have a choice parent element we cannot assume any valid information over the geometry type
1580       if ( attributeElement.parentNode().nodeName() == QLatin1String( "choice" ) && ! attributeElement.nextSibling().isNull() )
1581         geomType = QgsWkbTypes::Unknown;
1582       else
1583         geomType = geomTypeFromPropertyType( geometryAttribute, gmlPT.cap( 1 ) );
1584     }
1585     //MH 090428: sometimes the <element> tags for geometry attributes have only attribute ref="gml:polygonProperty"
1586     //Note: this was deprecated with GML3.
1587     else if ( ! foundGeometryAttribute &&  ref.indexOf( gmlRefProperty ) == 0 )
1588     {
1589       foundGeometryAttribute = true;
1590       geometryAttribute = ref.mid( 4 ); // Strip gml: prefix
1591       QString propertyType( gmlRefProperty.cap( 1 ) );
1592       // Set the first character in upper case
1593       propertyType = propertyType.at( 0 ).toUpper() + propertyType.mid( 1 );
1594       geomType = geomTypeFromPropertyType( geometryAttribute, propertyType );
1595     }
1596     else if ( !name.isEmpty() ) //todo: distinguish between numerical and non-numerical types
1597     {
1598       QVariant::Type  attributeType = QVariant::String; //string is default type
1599       if ( type.contains( QLatin1String( "double" ), Qt::CaseInsensitive ) || type.contains( QLatin1String( "float" ), Qt::CaseInsensitive ) || type.contains( QLatin1String( "decimal" ), Qt::CaseInsensitive ) )
1600       {
1601         attributeType = QVariant::Double;
1602       }
1603       else if ( type.contains( QLatin1String( "int" ), Qt::CaseInsensitive ) ||
1604                 type.contains( QLatin1String( "short" ), Qt::CaseInsensitive ) )
1605       {
1606         attributeType = QVariant::Int;
1607       }
1608       else if ( type.contains( QLatin1String( "long" ), Qt::CaseInsensitive ) )
1609       {
1610         attributeType = QVariant::LongLong;
1611       }
1612       else if ( type.contains( QLatin1String( "dateTime" ), Qt::CaseInsensitive ) )
1613       {
1614         attributeType = QVariant::DateTime;
1615       }
1616       fields.append( QgsField( name, attributeType, type ) );
1617     }
1618   }
1619   if ( !foundGeometryAttribute )
1620   {
1621     geomType = QgsWkbTypes::NoGeometry;
1622   }
1623 
1624   return true;
1625 }
1626 
name() const1627 QString QgsWFSProvider::name() const
1628 {
1629   return WFS_PROVIDER_KEY;
1630 }
1631 
description() const1632 QString QgsWFSProvider::description() const
1633 {
1634   return WFS_PROVIDER_DESCRIPTION;
1635 }
1636 
capabilities() const1637 QgsVectorDataProvider::Capabilities QgsWFSProvider::capabilities() const
1638 {
1639   return mCapabilities;
1640 }
1641 
sendTransactionDocument(const QDomDocument & doc,QDomDocument & serverResponse)1642 bool QgsWFSProvider::sendTransactionDocument( const QDomDocument &doc, QDomDocument &serverResponse )
1643 {
1644   if ( doc.isNull() )
1645   {
1646     return false;
1647   }
1648 
1649   QgsDebugMsgLevel( doc.toString(), 4 );
1650 
1651   QgsWFSTransactionRequest request( mShared->mURI );
1652   return request.send( doc, serverResponse );
1653 }
1654 
createTransactionElement(QDomDocument & doc) const1655 QDomElement QgsWFSProvider::createTransactionElement( QDomDocument &doc ) const
1656 {
1657   QDomElement transactionElem = doc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Transaction" ) );
1658   const QString WfsVersion = mShared->mWFSVersion;
1659   // only 1.1.0 and 1.0.0 are supported
1660   if ( WfsVersion == QStringLiteral( "1.1.0" ) )
1661   {
1662     transactionElem.setAttribute( QStringLiteral( "version" ), WfsVersion );
1663   }
1664   else
1665   {
1666     transactionElem.setAttribute( QStringLiteral( "version" ), QStringLiteral( "1.0.0" ) );
1667   }
1668   transactionElem.setAttribute( QStringLiteral( "service" ), QStringLiteral( "WFS" ) );
1669   transactionElem.setAttribute( QStringLiteral( "xmlns:xsi" ), QStringLiteral( "http://www.w3.org/2001/XMLSchema-instance" ) );
1670 
1671   QUrl describeFeatureTypeURL = mShared->mURI.requestUrl( QStringLiteral( "DescribeFeatureType" ) );
1672   // For tests (since the URL contains part of random data, we need to replace it with a fixed content)
1673   if ( describeFeatureTypeURL.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
1674   {
1675     describeFeatureTypeURL = QUrl( QStringLiteral( "http://fake_qgis_http_endpoint" ) );
1676     QUrlQuery query( describeFeatureTypeURL );
1677     query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) );
1678     describeFeatureTypeURL.setQuery( query );
1679   }
1680   QUrlQuery query( describeFeatureTypeURL );
1681   query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) );
1682   query.addQueryItem( QStringLiteral( "TYPENAME" ), mShared->mURI.typeName() );
1683   describeFeatureTypeURL.setQuery( query );
1684 
1685   transactionElem.setAttribute( QStringLiteral( "xsi:schemaLocation" ), mApplicationNamespace + ' '
1686                                 + describeFeatureTypeURL.toEncoded() );
1687 
1688   QString namespacePrefix = QgsWFSUtils::nameSpacePrefix( mShared->mURI.typeName() );
1689   if ( !namespacePrefix.isEmpty() )
1690   {
1691     transactionElem.setAttribute( "xmlns:" + namespacePrefix, mApplicationNamespace );
1692   }
1693   transactionElem.setAttribute( QStringLiteral( "xmlns:gml" ), QgsWFSConstants::GML_NAMESPACE );
1694 
1695   return transactionElem;
1696 }
1697 
transactionSuccess(const QDomDocument & serverResponse) const1698 bool QgsWFSProvider::transactionSuccess( const QDomDocument &serverResponse ) const
1699 {
1700   if ( serverResponse.isNull() )
1701   {
1702     return false;
1703   }
1704 
1705   QDomElement documentElem = serverResponse.documentElement();
1706   if ( documentElem.isNull() )
1707   {
1708     return false;
1709   }
1710 
1711   const QString WfsVersion = mShared->mWFSVersion;
1712 
1713   if ( WfsVersion == QStringLiteral( "1.1.0" ) )
1714   {
1715     const QDomNodeList transactionSummaryList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionSummary" ) );
1716     if ( transactionSummaryList.size() < 1 )
1717     {
1718       return false;
1719     }
1720 
1721     QDomElement transactionElement { transactionSummaryList.at( 0 ).toElement() };
1722     QDomNodeList totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalInserted" ) );
1723     QDomNodeList totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalUpdated" ) );
1724     QDomNodeList totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalDeleted" ) );
1725     if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 )
1726     {
1727       return true;
1728     }
1729     if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 )
1730     {
1731       return true;
1732     }
1733     if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 )
1734     {
1735       return true;
1736     }
1737 
1738     // Handle wrong QGIS server response (capital initial letter)
1739     totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalInserted" ) );
1740     totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalUpdated" ) );
1741     totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalDeleted" ) );
1742     if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 )
1743     {
1744       return true;
1745     }
1746     if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 )
1747     {
1748       return true;
1749     }
1750     if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 )
1751     {
1752       return true;
1753     }
1754 
1755     return false;
1756 
1757   }
1758   else
1759   {
1760     const QDomNodeList transactionResultList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionResult" ) );
1761     if ( transactionResultList.size() < 1 )
1762     {
1763       return false;
1764     }
1765 
1766     const QDomNodeList statusList = transactionResultList.at( 0 ).toElement().elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Status" ) );
1767     if ( statusList.size() < 1 )
1768     {
1769       return false;
1770     }
1771 
1772     return statusList.at( 0 ).firstChildElement().localName() == QLatin1String( "SUCCESS" );
1773   }
1774 
1775 }
1776 
insertedFeatureIds(const QDomDocument & serverResponse) const1777 QStringList QgsWFSProvider::insertedFeatureIds( const QDomDocument &serverResponse ) const
1778 {
1779   QStringList ids;
1780   if ( serverResponse.isNull() )
1781   {
1782     return ids;
1783   }
1784 
1785   QDomElement rootElem = serverResponse.documentElement();
1786   if ( rootElem.isNull() )
1787   {
1788     return ids;
1789   }
1790 
1791   // Handles WFS 1.1.0
1792   QString insertResultTagName;
1793   if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) )
1794   {
1795     insertResultTagName = QStringLiteral( "InsertResults" );
1796   }
1797   else
1798   {
1799     insertResultTagName = QStringLiteral( "InsertResult" );
1800   }
1801   QDomNodeList insertResultList = rootElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, insertResultTagName );
1802   for ( int i = 0; i < insertResultList.size(); ++i )
1803   {
1804     QDomNodeList featureIdList = insertResultList.at( i ).toElement().elementsByTagNameNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1805     for ( int j = 0; j < featureIdList.size(); ++j )
1806     {
1807       QString fidString = featureIdList.at( j ).toElement().attribute( QStringLiteral( "fid" ) );
1808       if ( !fidString.isEmpty() )
1809       {
1810         ids << fidString;
1811       }
1812     }
1813   }
1814   return ids;
1815 }
1816 
getCapabilities()1817 bool QgsWFSProvider::getCapabilities()
1818 {
1819   mCapabilities = QgsVectorDataProvider::SelectAtId;
1820 
1821   if ( mShared->mCaps.version.isEmpty() )
1822   {
1823     QgsWfsCapabilities getCapabilities( mShared->mURI.uri() );
1824     const bool synchronous = true;
1825     const bool forceRefresh = false;
1826     if ( !getCapabilities.requestCapabilities( synchronous, forceRefresh ) )
1827     {
1828       QgsMessageLog::logMessage( tr( "GetCapabilities failed for url %1: %2" ).
1829                                  arg( dataSourceUri(), getCapabilities.errorMessage() ), tr( "WFS" ) );
1830       return false;
1831     }
1832 
1833     const QgsWfsCapabilities::Capabilities caps = getCapabilities.capabilities();
1834     mShared->mCaps = caps;
1835   }
1836   mShared->mURI.setGetEndpoints( mShared->mCaps.operationGetEndpoints );
1837   mShared->mURI.setPostEndpoints( mShared->mCaps.operationPostEndpoints );
1838 
1839   mShared->mWFSVersion = mShared->mCaps.version;
1840   if ( mShared->mURI.maxNumFeatures() > 0 && mShared->mCaps.maxFeatures > 0 && !( mShared->mCaps.supportsPaging && mShared->mURI.pagingEnabled() ) )
1841   {
1842     mShared->mMaxFeatures = std::min( mShared->mURI.maxNumFeatures(), mShared->mCaps.maxFeatures );
1843   }
1844   else if ( mShared->mURI.maxNumFeatures() > 0 )
1845   {
1846     mShared->mMaxFeatures = mShared->mURI.maxNumFeatures();
1847   }
1848   else if ( mShared->mCaps.maxFeatures > 0 && !( mShared->mCaps.supportsPaging && mShared->mURI.pagingEnabled() ) )
1849   {
1850     mShared->mMaxFeatures = mShared->mCaps.maxFeatures;
1851   }
1852   else
1853   {
1854     mShared->mMaxFeatures = 0;
1855   }
1856 
1857   if ( mShared->mCaps.supportsPaging && mShared->mURI.pagingEnabled() )
1858   {
1859     if ( mShared->mURI.pageSize() > 0 )
1860     {
1861       if ( mShared->mCaps.maxFeatures > 0 )
1862       {
1863         mShared->mPageSize = std::min( mShared->mURI.pageSize(), mShared->mCaps.maxFeatures );
1864       }
1865       else
1866       {
1867         mShared->mPageSize = mShared->mURI.pageSize();
1868       }
1869     }
1870     else if ( mShared->mCaps.maxFeatures > 0 )
1871     {
1872       mShared->mPageSize = mShared->mCaps.maxFeatures;
1873     }
1874     else
1875     {
1876       QgsSettings settings;
1877       mShared->mPageSize = settings.value( QStringLiteral( "wfs/max_feature_count_if_not_provided" ), "1000" ).toInt();
1878       QgsDebugMsg( QStringLiteral( "Server declares paging but does not advertise max feature count and user did not specify it. Using %1" ).arg( mShared->mPageSize ) );
1879     }
1880   }
1881   else
1882   {
1883     mShared->mPageSize = 0;
1884   }
1885 
1886   //find the <FeatureType> for this layer
1887   QString thisLayerName = mShared->mURI.typeName();
1888   bool foundLayer = false;
1889   for ( int i = 0; i < mShared->mCaps.featureTypes.size(); i++ )
1890   {
1891     if ( thisLayerName == mShared->mCaps.featureTypes[i].name )
1892     {
1893       const QgsRectangle &r = mShared->mCaps.featureTypes[i].bbox;
1894       if ( mShared->mSourceCrs.authid().isEmpty() && mShared->mCaps.featureTypes[i].crslist.size() != 0 )
1895       {
1896         mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( mShared->mCaps.featureTypes[i].crslist[0] );
1897       }
1898       if ( !r.isNull() )
1899       {
1900         if ( mShared->mCaps.featureTypes[i].bboxSRSIsWGS84 )
1901         {
1902           QgsCoordinateReferenceSystem src = QgsCoordinateReferenceSystem::fromOgcWmsCrs( QStringLiteral( "CRS:84" ) );
1903           QgsCoordinateTransform ct( src, mShared->mSourceCrs, transformContext() );
1904           QgsDebugMsgLevel( "latlon ext:" + r.toString(), 4 );
1905           QgsDebugMsgLevel( "src:" + src.authid(), 4 );
1906           QgsDebugMsgLevel( "dst:" + mShared->mSourceCrs.authid(), 4 );
1907 
1908           mShared->mCapabilityExtent = ct.transformBoundingBox( r, QgsCoordinateTransform::ForwardTransform );
1909         }
1910         else
1911         {
1912           mShared->mCapabilityExtent = r;
1913         }
1914 
1915         QgsDebugMsgLevel( "layer ext:" + mShared->mCapabilityExtent.toString(), 4 );
1916       }
1917       if ( mShared->mCaps.featureTypes[i].insertCap )
1918       {
1919         mCapabilities |= QgsVectorDataProvider::AddFeatures;
1920       }
1921       if ( mShared->mCaps.featureTypes[i].updateCap )
1922       {
1923         mCapabilities |= QgsVectorDataProvider::ChangeAttributeValues;
1924         mCapabilities |= QgsVectorDataProvider::ChangeGeometries;
1925       }
1926       if ( mShared->mCaps.featureTypes[i].deleteCap )
1927       {
1928         mCapabilities |= QgsVectorDataProvider::DeleteFeatures;
1929       }
1930 
1931       foundLayer = true;
1932     }
1933   }
1934 
1935   if ( !foundLayer )
1936   {
1937     QgsMessageLog::logMessage( tr( "Could not find typename %1 in capabilities for url %2" ).
1938                                arg( thisLayerName, dataSourceUri() ), tr( "WFS" ) );
1939   }
1940 
1941   return foundLayer;
1942 }
1943 
geomTypeFromPropertyType(const QString & attName,const QString & propType)1944 QgsWkbTypes::Type QgsWFSProvider::geomTypeFromPropertyType( const QString &attName, const QString &propType )
1945 {
1946   Q_UNUSED( attName )
1947 
1948   QgsDebugMsgLevel( QStringLiteral( "DescribeFeatureType geometry attribute \"%1\" type is \"%2\"" )
1949                     .arg( attName, propType ), 4 );
1950   if ( propType == QLatin1String( "Point" ) )
1951     return QgsWkbTypes::Point;
1952   if ( propType == QLatin1String( "LineString" ) || propType == QLatin1String( "Curve" ) )
1953     return QgsWkbTypes::LineString;
1954   if ( propType == QLatin1String( "Polygon" ) || propType == QLatin1String( "Surface" ) )
1955     return QgsWkbTypes::Polygon;
1956   if ( propType == QLatin1String( "MultiPoint" ) )
1957     return QgsWkbTypes::MultiPoint;
1958   if ( propType == QLatin1String( "MultiLineString" ) || propType == QLatin1String( "MultiCurve" ) )
1959     return QgsWkbTypes::MultiLineString;
1960   if ( propType == QLatin1String( "MultiPolygon" ) || propType == QLatin1String( "MultiSurface" ) )
1961     return QgsWkbTypes::MultiPolygon;
1962   return QgsWkbTypes::Unknown;
1963 }
1964 
handleException(const QDomDocument & serverResponse)1965 void QgsWFSProvider::handleException( const QDomDocument &serverResponse )
1966 {
1967   QgsDebugMsgLevel( QStringLiteral( "server response: %1" ).arg( serverResponse.toString() ), 4 );
1968 
1969   QDomElement exceptionElem = serverResponse.documentElement();
1970   if ( exceptionElem.isNull() )
1971   {
1972     pushError( tr( "Empty response" ) );
1973     return;
1974   }
1975 
1976   if ( exceptionElem.tagName() == QLatin1String( "ServiceExceptionReport" ) )
1977   {
1978     pushError( tr( "WFS service exception: %1" ).arg( exceptionElem.firstChildElement( QStringLiteral( "ServiceException" ) ).text() ) );
1979     return;
1980   }
1981 
1982   if ( exceptionElem.tagName() == QLatin1String( "WFS_TransactionResponse" ) )
1983   {
1984     pushError( tr( "Unsuccessful service response: %1" ).arg( exceptionElem.firstChildElement( QStringLiteral( "TransactionResult" ) ).firstChildElement( QStringLiteral( "Message" ) ).text() ) );
1985     return;
1986   }
1987 
1988   // WFS 1.1.0
1989   if ( exceptionElem.tagName() == QLatin1String( "TransactionResponse" ) )
1990   {
1991     pushError( tr( "Unsuccessful service response: no features were added, deleted or changed." ) );
1992     return;
1993   }
1994 
1995 
1996   if ( exceptionElem.tagName() == QLatin1String( "ExceptionReport" ) )
1997   {
1998     QDomElement exception = exceptionElem.firstChildElement( QStringLiteral( "Exception" ) );
1999     // The XML schema at http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd uses
2000     // the "exceptionCode" attribute, but http://docs.opengeospatial.org/is/04-094r1/04-094r1.html#36
2001     // mentions "code". Accept both...
2002     pushError( tr( "WFS exception report (code=%1 text=%2)" )
2003                .arg( exception.attribute( QStringLiteral( "exceptionCode" ),
2004                                           exception.attribute( QStringLiteral( "code" ), tr( "missing" ) ) ),
2005                      exception.firstChildElement( QStringLiteral( "ExceptionText" ) ).text() )
2006              );
2007     return;
2008   }
2009 
2010   pushError( tr( "Unhandled response: %1" ).arg( exceptionElem.tagName() ) );
2011 }
2012 
createProvider(const QString & uri,const QgsDataProvider::ProviderOptions & options,QgsDataProvider::ReadFlags flags)2013 QgsWFSProvider *QgsWfsProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags )
2014 {
2015   Q_UNUSED( flags );
2016   return new QgsWFSProvider( uri, options );
2017 }
2018 
dataItemProviders() const2019 QList<QgsDataItemProvider *> QgsWfsProviderMetadata::dataItemProviders() const
2020 {
2021   QList<QgsDataItemProvider *> providers;
2022   providers << new QgsWfsDataItemProvider;
2023   return providers;
2024 }
2025 
2026 
QgsWfsProviderMetadata()2027 QgsWfsProviderMetadata::QgsWfsProviderMetadata():
2028   QgsProviderMetadata( QgsWFSProvider::WFS_PROVIDER_KEY, QgsWFSProvider::WFS_PROVIDER_DESCRIPTION ) {}
2029 
multipleProviderMetadataFactory()2030 QGISEXTERN void *multipleProviderMetadataFactory()
2031 {
2032   return new std::vector<QgsProviderMetadata *> { new QgsWfsProviderMetadata(), new QgsOapifProviderMetadata() };
2033 }
2034