diff --git a/python/server/qgsfeaturefilter.sip b/python/server/qgsfeaturefilter.sip new file mode 100644 index 00000000000..3e0f4e3d3c6 --- /dev/null +++ b/python/server/qgsfeaturefilter.sip @@ -0,0 +1,60 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsfeaturefilter.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsFeatureFilter : QgsFeatureFilterProvider +{ +%Docstring + A feature filter provider allowing to set filter expressions on a per-layer basis. +.. versionadded:: 3.0 +* +%End + +%TypeHeaderCode +#include "qgsfeaturefilter.h" +%End + public: + QgsFeatureFilter(); +%Docstring +Constructor +%End + + void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const; +%Docstring + Filter the features of the layer + \param layer the layer to control + \param filterFeatures the request to fill +%End + + QgsFeatureFilterProvider *clone() const /Factory/; +%Docstring + Return a clone of the object + :return: A clone + :rtype: QgsFeatureFilterProvider +%End + + void setFilter( const QgsVectorLayer *layer, const QgsExpression &expression ); +%Docstring + Set a filter for the given layer. + \param layer the layer to filter + \param expression the filter expression +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsfeaturefilter.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/server/qgsfeaturefilterprovidergroup.sip b/python/server/qgsfeaturefilterprovidergroup.sip new file mode 100644 index 00000000000..be98143c4f9 --- /dev/null +++ b/python/server/qgsfeaturefilterprovidergroup.sip @@ -0,0 +1,60 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsfeaturefilterprovidergroup.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsFeatureFilterProviderGroup : QgsFeatureFilterProvider +{ +%Docstring + A filter filter provider grouping several filter providers. +.. versionadded:: 3.0 +* +%End + +%TypeHeaderCode +#include "qgsfeaturefilterprovidergroup.h" +%End + public: + QgsFeatureFilterProviderGroup(); +%Docstring +Constructor +%End + + void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const; +%Docstring + Filter the features of the layer + \param layer the layer to control + \param filterFeatures the request to fill +%End + + QgsFeatureFilterProvider *clone() const /Factory/; +%Docstring + Return a clone of the object + :return: A clone + :rtype: QgsFeatureFilterProvider +%End + + QgsFeatureFilterProviderGroup &addProvider( const QgsFeatureFilterProvider *provider ); +%Docstring + Add another filter provider to the group + \param provider The provider to add + :return: itself + :rtype: QgsFeatureFilterProviderGroup +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsfeaturefilterprovidergroup.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/server/server_auto.sip b/python/server/server_auto.sip index 5372fb43e29..f4d22245cc2 100644 --- a/python/server/server_auto.sip +++ b/python/server/server_auto.sip @@ -17,6 +17,8 @@ %Include qgsservice.sip %Include qgsservicemodule.sip %Include qgsserviceregistry.sip +%Include qgsfeaturefilterprovidergroup.sip +%Include qgsfeaturefilter.sip %If ( HAVE_SERVER_PYTHON_PLUGINS ) %Include qgsserverfilter.sip %End diff --git a/src/core/qgsfeaturerequest.h b/src/core/qgsfeaturerequest.h index a189b1b1d80..92e9a01c9c4 100644 --- a/src/core/qgsfeaturerequest.h +++ b/src/core/qgsfeaturerequest.h @@ -428,7 +428,7 @@ class CORE_EXPORT QgsFeatureRequest * * \since QGIS 2.12 */ - QgsFeatureRequest &disableFilter() { mFilter = FilterNone; return *this; } + QgsFeatureRequest &disableFilter() { mFilter = FilterNone; mFilterExpression.reset(); return *this; } /** * Adds a new OrderByClause, appending it as the least important one. diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 806276fdd2a..7af8f3b09a5 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -44,6 +44,8 @@ SET(QGIS_SERVER_SRCS qgsservicemodule.cpp qgsservicenativeloader.cpp qgsserviceregistry.cpp + qgsfeaturefilterprovidergroup.cpp + qgsfeaturefilter.cpp ) SET (QGIS_SERVER_HDRS diff --git a/src/server/qgsfeaturefilter.cpp b/src/server/qgsfeaturefilter.cpp new file mode 100644 index 00000000000..d821e83fc06 --- /dev/null +++ b/src/server/qgsfeaturefilter.cpp @@ -0,0 +1,42 @@ +/*************************************************************************** + qgsfeaturefilter.cpp + -------------------- + begin : 26-10-2017 + copyright : (C) 2017 by Patrick Valsecchi + email : patrick dot valsecchi at camptocamp dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsfeaturefilter.h" +#include "qgsfeaturerequest.h" +#include "qgsvectorlayer.h" +#include "qgsexpression.h" + +void QgsFeatureFilter::filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const +{ + const QString expr = mFilters[layer->id()]; + if ( !expr.isEmpty() ) + { + filterFeatures.setFilterExpression( expr ); + } +} + +QgsFeatureFilterProvider *QgsFeatureFilter::clone() const +{ + auto result = new QgsFeatureFilter(); + result->mFilters = mFilters; + return result; +} + +void QgsFeatureFilter::setFilter( const QgsVectorLayer *layer, const QgsExpression &filter ) +{ + mFilters[layer->id()] = filter.dump(); +} diff --git a/src/server/qgsfeaturefilter.h b/src/server/qgsfeaturefilter.h new file mode 100644 index 00000000000..c351bd6d093 --- /dev/null +++ b/src/server/qgsfeaturefilter.h @@ -0,0 +1,64 @@ +/*************************************************************************** + qgsfeaturefilter.h + ------------------ + begin : 26-10-2017 + copyright : (C) 2017 by Patrick Valsecchi + email : patrick dot valsecchi at camptocamp dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSFEATUREFILTER_H +#define QGSFEATUREFILTER_H + +#include "qgsfeaturefilterprovider.h" +#include "qgis_server.h" + +#include + +class QgsExpression; + +/** + * \ingroup server + * \class QgsFeatureFilter + * \brief A feature filter provider allowing to set filter expressions on a per-layer basis. + * \since QGIS 3.0 + **/ +class SERVER_EXPORT QgsFeatureFilter : public QgsFeatureFilterProvider +{ + public: + //! Constructor + QgsFeatureFilter() {} + + /** + * Filter the features of the layer + * \param layer the layer to control + * \param filterFeatures the request to fill + */ + void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const; + + /** + * Return a clone of the object + * \returns A clone + */ + QgsFeatureFilterProvider *clone() const SIP_FACTORY; + + /** + * Set a filter for the given layer. + * \param layer the layer to filter + * \param expression the filter expression + */ + void setFilter( const QgsVectorLayer *layer, const QgsExpression &expression ); + + private: + QMap mFilters; +}; + +#endif diff --git a/src/server/qgsfeaturefilterprovidergroup.cpp b/src/server/qgsfeaturefilterprovidergroup.cpp new file mode 100644 index 00000000000..1e87da9cbdf --- /dev/null +++ b/src/server/qgsfeaturefilterprovidergroup.cpp @@ -0,0 +1,49 @@ +/*************************************************************************** + qgsfeaturefilterprovidergroup.cpp + -------------------------------- + begin : 26-10-2017 + copyright : (C) 2017 by Patrick Valsecchi + email : patrick dot valsecchi at camptocamp dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsfeaturefilterprovidergroup.h" +#include "qgsfeaturerequest.h" + +void QgsFeatureFilterProviderGroup::filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const +{ + filterFeatures.disableFilter(); + for ( const QgsFeatureFilterProvider *provider : mProviders ) + { + QgsFeatureRequest temp; + provider->filterFeatures( layer, temp ); + if ( temp.filterExpression() ) + { + filterFeatures.combineFilterExpression( temp.filterExpression()->dump() ); + } + } +} + +QgsFeatureFilterProvider *QgsFeatureFilterProviderGroup::clone() const +{ + auto result = new QgsFeatureFilterProviderGroup(); + result->mProviders = mProviders; + return result; +} + +QgsFeatureFilterProviderGroup &QgsFeatureFilterProviderGroup::addProvider( const QgsFeatureFilterProvider *provider ) +{ + if ( provider ) + { + mProviders.append( provider ); + } + return *this; +} diff --git a/src/server/qgsfeaturefilterprovidergroup.h b/src/server/qgsfeaturefilterprovidergroup.h new file mode 100644 index 00000000000..bb318f0eba7 --- /dev/null +++ b/src/server/qgsfeaturefilterprovidergroup.h @@ -0,0 +1,62 @@ +/*************************************************************************** + qgsfeaturefilterprovidergroup.h + ------------------------------- + begin : 26-10-2017 + copyright : (C) 2017 by Patrick Valsecchi + email : patrick dot valsecchi at camptocamp dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSFEATUREFILTERPROVIDERGROUP_H +#define QGSFEATUREFILTERPROVIDERGROUP_H + +#include "qgsfeaturefilterprovider.h" +#include "qgis_server.h" + +#include + +/** + * \ingroup server + * \class QgsFeatureFilterProviderGroup + * \brief A filter filter provider grouping several filter providers. + * \since QGIS 3.0 + **/ +class SERVER_EXPORT QgsFeatureFilterProviderGroup : public QgsFeatureFilterProvider +{ + public: + //! Constructor + QgsFeatureFilterProviderGroup() {} + + /** + * Filter the features of the layer + * \param layer the layer to control + * \param filterFeatures the request to fill + */ + void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const; + + /** + * Return a clone of the object + * \returns A clone + */ + QgsFeatureFilterProvider *clone() const SIP_FACTORY; + + /** + * Add another filter provider to the group + * \param provider The provider to add + * \return itself + */ + QgsFeatureFilterProviderGroup &addProvider( const QgsFeatureFilterProvider *provider ); + + private: + QList mProviders; +}; + +#endif diff --git a/src/server/services/wms/qgsmaprendererjobproxy.cpp b/src/server/services/wms/qgsmaprendererjobproxy.cpp index ac5856053ac..51a0b5e0c28 100644 --- a/src/server/services/wms/qgsmaprendererjobproxy.cpp +++ b/src/server/services/wms/qgsmaprendererjobproxy.cpp @@ -27,14 +27,14 @@ namespace QgsWms QgsMapRendererJobProxy::QgsMapRendererJobProxy( bool parallelRendering , int maxThreads - , QgsAccessControl *accessControl + , QgsFeatureFilterProvider *featureFilterProvider ) : mParallelRendering( parallelRendering ) - , mAccessControl( accessControl ) + , mFeatureFilterProvider( featureFilterProvider ) { #ifndef HAVE_SERVER_PYTHON_PLUGINS - Q_UNUSED( mAccessControl ); + Q_UNUSED( mFeatureFilterProvider ); #endif if ( mParallelRendering ) { @@ -53,7 +53,7 @@ namespace QgsWms { QgsMapRendererParallelJob renderJob( mapSettings ); #ifdef HAVE_SERVER_PYTHON_PLUGINS - renderJob.setFeatureFilterProvider( mAccessControl ); + renderJob.setFeatureFilterProvider( mFeatureFilterProvider ); #endif renderJob.start(); renderJob.waitForFinished(); @@ -65,7 +65,7 @@ namespace QgsWms mPainter.reset( new QPainter( image ) ); QgsMapRendererCustomPainterJob renderJob( mapSettings, mPainter.get() ); #ifdef HAVE_SERVER_PYTHON_PLUGINS - renderJob.setFeatureFilterProvider( mAccessControl ); + renderJob.setFeatureFilterProvider( mFeatureFilterProvider ); #endif renderJob.renderSynchronously(); } diff --git a/src/server/services/wms/qgsmaprendererjobproxy.h b/src/server/services/wms/qgsmaprendererjobproxy.h index 75d691b1869..6435212e118 100644 --- a/src/server/services/wms/qgsmaprendererjobproxy.h +++ b/src/server/services/wms/qgsmaprendererjobproxy.h @@ -19,7 +19,8 @@ #define QGSMAPRENDERERJOBPROXY_H #include "qgsmapsettings.h" -#include "qgsaccesscontrol.h" + +class QgsFeatureFilterProvider; namespace QgsWms { @@ -36,12 +37,12 @@ namespace QgsWms /** * Constructor. - * \param accessControl Does not take ownership of QgsAccessControl + * \param featureFilterProvider Does not take ownership of QgsFeatureFilterProvider */ QgsMapRendererJobProxy( bool parallelRendering , int maxThreads - , QgsAccessControl *accessControl + , QgsFeatureFilterProvider *featureFilterProvider ); /** @@ -59,7 +60,7 @@ namespace QgsWms private: bool mParallelRendering; - QgsAccessControl *mAccessControl = nullptr; + QgsFeatureFilterProvider *mFeatureFilterProvider = nullptr; std::unique_ptr mPainter; }; diff --git a/src/server/services/wms/qgswmsparameters.cpp b/src/server/services/wms/qgswmsparameters.cpp index d1290c39aa9..5fd10b2e2ce 100644 --- a/src/server/services/wms/qgswmsparameters.cpp +++ b/src/server/services/wms/qgswmsparameters.cpp @@ -488,7 +488,7 @@ namespace QgsWms mRequestParameters = parameters; const QMetaEnum metaEnum( QMetaEnum::fromType() ); - static QRegExp composerParamRegExp( "^MAP\\d+:" ); + static QRegExp composerParamRegExp( QStringLiteral( "^MAP\\d+:" ) ); foreach ( QString key, parameters.keys() ) { @@ -529,7 +529,7 @@ namespace QgsWms } else //maybe an external wms parameter? { - int separator = key.indexOf( ":" ); + int separator = key.indexOf( QStringLiteral( ":" ) ); if ( separator >= 1 ) { QString id = key.left( separator ); @@ -545,7 +545,7 @@ namespace QgsWms { const QMetaEnum metaEnum( QMetaEnum::fromType() ); - log( "WMS Request parameters:" ); + log( QStringLiteral( "WMS Request parameters:" ) ); for ( auto parameter : mParameters.toStdMap() ) { const QString value = parameter.second.mValue.toString(); @@ -553,13 +553,13 @@ namespace QgsWms if ( ! value.isEmpty() ) { const QString name = metaEnum.valueToKey( parameter.first ); - log( " - " + name + " : " + value ); + log( QStringLiteral( " - " ) + name + QStringLiteral( " : " ) + value ); } } for ( auto map : mComposerParameters.toStdMap() ) { const int mapId = map.first; - log( " - MAP" + QString::number( mapId ) ); + log( QStringLiteral( " - MAP" ) + QString::number( mapId ) ); for ( auto param : mComposerParameters[map.first].toStdMap() ) { const QString value = param.second.mValue.toString(); @@ -567,13 +567,13 @@ namespace QgsWms if ( ! value.isEmpty() ) { const QString name = metaEnum.valueToKey( param.first ); - log( " - MAP" + QString::number( mapId ) + ":" + name + " : " + value ); + log( QStringLiteral( " - MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + name + QStringLiteral( " : " ) + value ); } } } if ( !version().isEmpty() ) - log( " - VERSION : " + version() ); + log( QStringLiteral( " - VERSION : " ) + version() ); } void QgsWmsParameters::save( const Parameter ¶meter ) @@ -694,8 +694,8 @@ namespace QgsWms { // VERSION parameter is not managed with other parameters because // there's a conflict with qgis VERSION defined in qgsconfig.h - if ( mRequestParameters.contains( "VERSION" ) ) - return mRequestParameters["VERSION"]; + if ( mRequestParameters.contains( QStringLiteral( "VERSION" ) ) ) + return mRequestParameters[QStringLiteral( "VERSION" )]; else return QString(); } @@ -736,7 +736,7 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into a double"; + QString msg = n + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a double" ); raiseError( msg ); } @@ -751,7 +751,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p, mapId ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into a double"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a double" ); raiseError( msg ); } @@ -802,7 +803,7 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into int"; + QString msg = n + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into int" ); raiseError( msg ); } @@ -817,7 +818,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p, mapId ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into int"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into int" ); raiseError( msg ); } @@ -833,8 +835,8 @@ namespace QgsWms if ( !cStr.isEmpty() ) { // support hexadecimal notation to define colors - if ( cStr.startsWith( "0x", Qt::CaseInsensitive ) ) - cStr.replace( 0, 2, "#" ); + if ( cStr.startsWith( QStringLiteral( "0x" ), Qt::CaseInsensitive ) ) + cStr.replace( 0, 2, QStringLiteral( "#" ) ); c = QColor( cStr ); @@ -852,7 +854,7 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into a color"; + QString msg = n + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a color" ); raiseError( msg ); } @@ -867,7 +869,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p, mapId ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into a color"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a color" ); raiseError( msg ); } @@ -926,7 +929,7 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into a rectangle"; + QString msg = n + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a rectangle" ); raiseError( msg ); } @@ -941,7 +944,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p, mapId ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into a rectangle"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a rectangle" ); raiseError( msg ); } @@ -990,7 +994,7 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into a list of int"; + QString msg = n + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a list of int" ); raiseError( msg ); } @@ -1005,7 +1009,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p, mapId ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into a list of int"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a list of int" ); raiseError( msg ); } @@ -1044,7 +1049,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into a list of float"; + QString msg = n + QStringLiteral( " ('" ) + valStr + + QStringLiteral( "') cannot be converted into a list of float" ); raiseError( msg ); } @@ -1059,7 +1065,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into a list of float"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a list of float" ); raiseError( msg ); } @@ -1097,7 +1104,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into a list of colors"; + QString msg = n + QStringLiteral( " ('" ) + valStr + + QStringLiteral( "') cannot be converted into a list of colors" ); raiseError( msg ); } @@ -1112,7 +1120,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into a list of colors"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a list of colors" ); raiseError( msg ); } @@ -1150,7 +1159,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p ).toString(); - QString msg = n + " ('" + valStr + "') cannot be converted into a list of geometries"; + QString msg = n + QStringLiteral( " ('" ) + valStr + + QStringLiteral( "') cannot be converted into a list of geometries" ); raiseError( msg ); } @@ -1165,7 +1175,8 @@ namespace QgsWms { QString n = name( p ); QString valStr = value( p, mapId ).toString(); - QString msg = "MAP" + QString::number( mapId ) + ":" + n + " ('" + valStr + "') cannot be converted into a list of geometries"; + QString msg = QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + n + + QStringLiteral( " ('" ) + valStr + QStringLiteral( "') cannot be converted into a list of geometries" ); raiseError( msg ); } @@ -1614,7 +1625,30 @@ namespace QgsWms QStringList QgsWmsParameters::filters() const { - return toStringList( ParameterName::FILTER, ';' ); + const QString filter = value( ParameterName::FILTER ).toString(); + if ( filter.startsWith( QStringLiteral( "(<" ) ) && filter.endsWith( QStringLiteral( "Filter>)" ) ) ) + { + // OGC filter on multiple layers + // remove the "(<" at the beginning and the "Filter>)" at the end + const QString toSplit = filter.mid( 2, filter.length() - 10 ); + + QStringList result; + for ( const QString &cur : toSplit.split( QStringLiteral( "Filter>)(<" ), QString::SkipEmptyParts ) ) + { + result.append( QStringLiteral( "<" ) + cur + QStringLiteral( "Filter>" ) ); + } + return result; + } + else if ( filter.startsWith( QStringLiteral( "<" ) ) && filter.endsWith( QStringLiteral( "Filter>" ) ) ) + { + // single OGC filter + return QStringList( filter ); + } + else + { + // QGIS specific filter + return filter.split( ';', QString::SkipEmptyParts ); + } } QString QgsWmsParameters::filterGeom() const @@ -1656,36 +1690,48 @@ namespace QgsWms return style << styles; } - QList QgsWmsParameters::layersParameters() const + QMultiMap QgsWmsParameters::getLayerFilters( const QStringList &layers ) const { - QList parameters; - QStringList layers = allLayersNickname(); - QStringList styles = allStyles(); - QStringList filter = filters(); - QStringList selection = selections(); - QList opacities = opacitiesAsInt(); - // filter format: "LayerName:filterString;LayerName2:filterString2;..." // several filters can be defined for one layer + const QStringList rawFilters = filters(); QMultiMap layerFilters; - Q_FOREACH ( QString f, filter ) + for ( int i = 0; i < rawFilters.size(); i++ ) { - const QStringList splits = f.split( ':' ); - if ( splits.size() == 2 ) + const QString f = rawFilters[i]; + if ( f.startsWith( QStringLiteral( "<" ) ) && f.endsWith( QStringLiteral( "Filter>" ) ) && i < layers.size() ) { - layerFilters.insert( splits[0], splits[1] ); + layerFilters.insert( layers[i], f ); } else { - QString filterStr = value( ParameterName::FILTER ).toString(); - raiseError( "FILTER ('" + filterStr + "') is not properly formatted" ); + const QStringList splits = f.split( ':' ); + if ( splits.size() == 2 ) + { + layerFilters.insert( splits[0], splits[1] ); + } + else + { + QString filterStr = value( ParameterName::FILTER ).toString(); + raiseError( QStringLiteral( "FILTER ('" ) + filterStr + QStringLiteral( "') is not properly formatted" ) ); + } } } + return layerFilters; + } + + QList QgsWmsParameters::layersParameters() const + { + const QStringList layers = allLayersNickname(); + const QStringList styles = allStyles(); + const QStringList selection = selections(); + const QList opacities = opacitiesAsInt(); + const QMultiMap layerFilters = getLayerFilters( layers ); // selection format: "LayerName:id0,id1;LayerName2:id0,id1;..." // several filters can be defined for one layer QMultiMap layerSelections; - Q_FOREACH ( QString s, selection ) + for ( const QString &s : selection ) { const QStringList splits = s.split( ':' ); if ( splits.size() == 2 ) @@ -1695,10 +1741,11 @@ namespace QgsWms else { QString selStr = value( ParameterName::SELECTION ).toString(); - raiseError( "SELECTION ('" + selStr + "') is not properly formatted" ); + raiseError( QStringLiteral( "SELECTION ('" ) + selStr + QStringLiteral( "') is not properly formatted" ) ); } } + QList parameters; for ( int i = 0; i < layers.size(); i++ ) { QString layer = layers[i]; @@ -1756,7 +1803,7 @@ namespace QgsWms for ( int i = 0; i < nLayers; i++ ) { QgsWmsParametersHighlightLayer param; - param.mName = "highlight_" + QString::number( i ); + param.mName = QStringLiteral( "highlight_" ) + QString::number( i ); param.mGeom = geoms[i]; param.mSld = slds[i]; @@ -1812,7 +1859,7 @@ namespace QgsWms if ( extentStr.isEmpty() ) return param; - QString pMapId = "MAP" + QString::number( mapId ); + QString pMapId = QStringLiteral( "MAP" ) + QString::number( mapId ); QgsRectangle extent = toRectangle( ParameterName::EXTENT, mapId ); if ( extent.isEmpty() ) @@ -1873,7 +1920,7 @@ namespace QgsWms for ( int i = 0; i < nHLayers; i++ ) { QgsWmsParametersHighlightLayer hParam; - hParam.mName = pMapId + "_highlight_" + QString::number( i ); + hParam.mName = pMapId + QStringLiteral( "_highlight_" ) + QString::number( i ); hParam.mGeom = geoms[i]; hParam.mSld = slds[i]; @@ -1940,7 +1987,7 @@ namespace QgsWms void QgsWmsParameters::log( const QString &msg ) const { - QgsMessageLog::logMessage( msg, "Server", QgsMessageLog::INFO ); + QgsMessageLog::logMessage( msg, QStringLiteral( "Server" ), QgsMessageLog::INFO ); } void QgsWmsParameters::raiseError( ParameterName paramName ) const @@ -1948,7 +1995,7 @@ namespace QgsWms const QString value = mParameters[paramName].mValue.toString(); const QString param = name( paramName ); const QString type = QVariant::typeToName( mParameters[paramName].mType ); - raiseError( param + " ('" + value + "') cannot be converted into " + type ); + raiseError( param + QStringLiteral( " ('" ) + value + QStringLiteral( "') cannot be converted into " ) + type ); } void QgsWmsParameters::raiseError( ParameterName paramName, int mapId ) const @@ -1956,7 +2003,9 @@ namespace QgsWms const QString value = mComposerParameters[mapId][paramName].mValue.toString(); const QString param = name( paramName ); const QString type = QVariant::typeToName( mComposerParameters[mapId][paramName].mType ); - raiseError( "MAP" + QString::number( mapId ) + ":" + param + " ('" + value + "') cannot be converted into " + type ); + raiseError( QStringLiteral( "MAP" ) + QString::number( mapId ) + QStringLiteral( ":" ) + + param + QStringLiteral( " ('" ) + value + + QStringLiteral( "') cannot be converted into " ) + type ); } void QgsWmsParameters::raiseError( const QString &msg ) const diff --git a/src/server/services/wms/qgswmsparameters.h b/src/server/services/wms/qgswmsparameters.h index a6ab78783f7..016c49c0e95 100644 --- a/src/server/services/wms/qgswmsparameters.h +++ b/src/server/services/wms/qgswmsparameters.h @@ -972,6 +972,7 @@ namespace QgsWms QList toGeomList( const QStringList &l, bool *error = Q_NULLPTR ) const; QList toGeomList( const QStringList &l, ParameterName name ) const; QList toGeomList( const QStringList &l, ParameterName name, int mapId ) const; + QMultiMap getLayerFilters( const QStringList &layers ) const; QgsServerRequest::Parameters mRequestParameters; QMap mParameters; diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index 5cd7203b448..83440099061 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -89,6 +89,8 @@ #include "qgscomposerpicture.h" #include "qgscomposerscalebar.h" #include "qgscomposershape.h" +#include "qgsfeaturefilterprovidergroup.h" +#include "qgsogcutils.h" #include #include #include @@ -919,21 +921,10 @@ namespace QgsWms } // The I/J parameters are Mandatory if they are not replaced by X/Y or FILTER or FILTER_GEOM - bool ijDefined = false; - if ( !mWmsParameters.i().isEmpty() && !mWmsParameters.j().isEmpty() ) - ijDefined = true; - - bool xyDefined = false; - if ( !mWmsParameters.x().isEmpty() && !mWmsParameters.y().isEmpty() ) - xyDefined = true; - - bool filtersDefined = false; - if ( !mWmsParameters.filters().isEmpty() ) - filtersDefined = true; - - bool filterGeomDefined = false; - if ( !mWmsParameters.filterGeom().isEmpty() ) - filterGeomDefined = true; + const bool ijDefined = !mWmsParameters.i().isEmpty() && !mWmsParameters.j().isEmpty(); + const bool xyDefined = !mWmsParameters.x().isEmpty() && !mWmsParameters.y().isEmpty(); + const bool filtersDefined = !mWmsParameters.filters().isEmpty(); + const bool filterGeomDefined = !mWmsParameters.filterGeom().isEmpty(); if ( !ijDefined && !xyDefined && !filtersDefined && !filterGeomDefined ) { @@ -2694,10 +2685,13 @@ namespace QgsWms } else { + QgsFeatureFilterProviderGroup filters; + filters.addProvider( &mFeatureFilter ); #ifdef HAVE_SERVER_PYTHON_PLUGINS mAccessControl->resolveFilterFeatures( mapSettings.layers() ); + filters.addProvider( mAccessControl ); #endif - QgsMapRendererJobProxy renderJob( mSettings.parallelRendering(), mSettings.maxThreads(), mAccessControl ); + QgsMapRendererJobProxy renderJob( mSettings.parallelRendering(), mSettings.maxThreads(), &filters ); renderJob.render( mapSettings, &image ); painter = renderJob.takePainter(); } @@ -2723,34 +2717,53 @@ namespace QgsWms } } - void QgsRenderer::setLayerFilter( QgsMapLayer *layer, const QStringList &filters ) const + void QgsRenderer::setLayerFilter( QgsMapLayer *layer, const QStringList &filters ) { if ( layer->type() == QgsMapLayer::VectorLayer ) { QgsVectorLayer *filteredLayer = qobject_cast( layer ); Q_FOREACH ( QString filter, filters ) { - if ( !testFilterStringSafety( filter ) ) + if ( filter.startsWith( QStringLiteral( "<" ) ) && filter.endsWith( QStringLiteral( "Filter>" ) ) ) { - throw QgsBadRequestException( QStringLiteral( "Filter string rejected" ), - QStringLiteral( "The filter string %1" - " has been rejected because of security reasons." - " Note: Text strings have to be enclosed in single or double quotes." - " A space between each word / special character is mandatory." - " Allowed Keywords and special characters are " - " AND,OR,IN,<,>=,>,>=,!=,',',(,),DMETAPHONE,SOUNDEX." - " Not allowed are semicolons in the filter expression." ).arg( filter ) ); + // OGC filter + QDomDocument filterXml; + QString errorMsg; + if ( !filterXml.setContent( filter, true, &errorMsg ) ) + { + throw QgsBadRequestException( QStringLiteral( "Filter string rejected" ), + QStringLiteral( "error message: %1. The XML string was: %2" ).arg( errorMsg, filter ) ); + } + QDomElement filterElem = filterXml.firstChildElement(); + QScopedPointer expression( QgsOgcUtils::expressionFromOgcFilter( filterElem ) ); + mFeatureFilter.setFilter( filteredLayer, *expression ); } + else + { + // QGIS (SQL) filter + if ( !testFilterStringSafety( filter ) ) + { + throw QgsBadRequestException( QStringLiteral( "Filter string rejected" ), + QStringLiteral( "The filter string %1" + " has been rejected because of security reasons." + " Note: Text strings have to be enclosed in single or double quotes." + " A space between each word / special character is mandatory." + " Allowed Keywords and special characters are " + " AND,OR,IN,<,>=,>,>=,!=,',',(,),DMETAPHONE,SOUNDEX." + " Not allowed are semicolons in the filter expression." ).arg( + filter ) ); + } - QString newSubsetString = filter; - if ( !filteredLayer->subsetString().isEmpty() ) - { - newSubsetString.prepend( ") AND (" ); - newSubsetString.append( ")" ); - newSubsetString.prepend( filteredLayer->subsetString() ); - newSubsetString.prepend( "(" ); + QString newSubsetString = filter; + if ( !filteredLayer->subsetString().isEmpty() ) + { + newSubsetString.prepend( ") AND (" ); + newSubsetString.append( ")" ); + newSubsetString.prepend( filteredLayer->subsetString() ); + newSubsetString.prepend( "(" ); + } + filteredLayer->setSubsetString( newSubsetString ); } - filteredLayer->setSubsetString( newSubsetString ); } } } diff --git a/src/server/services/wms/qgswmsrenderer.h b/src/server/services/wms/qgswmsrenderer.h index 21340d8f4de..862f91c4538 100644 --- a/src/server/services/wms/qgswmsrenderer.h +++ b/src/server/services/wms/qgswmsrenderer.h @@ -22,6 +22,7 @@ #include "qgsserversettings.h" #include "qgswmsparameters.h" +#include "qgsfeaturefilter.h" #include #include #include @@ -165,7 +166,7 @@ namespace QgsWms void setLayerOpacity( QgsMapLayer *layer, int opacity ) const; // Set layer filter - void setLayerFilter( QgsMapLayer *layer, const QStringList &filter ) const; + void setLayerFilter( QgsMapLayer *layer, const QStringList &filter ); // Set layer python filter void setLayerAccessControlFilter( QgsMapLayer *layer ) const; @@ -287,6 +288,7 @@ namespace QgsWms //! The access control helper QgsAccessControl *mAccessControl = nullptr; + QgsFeatureFilter mFeatureFilter; const QgsServerSettings &mSettings; const QgsProject *mProject = nullptr; diff --git a/tests/src/python/test_qgsserver_accesscontrol.py b/tests/src/python/test_qgsserver_accesscontrol.py index 6299c50d617..550730e6f96 100644 --- a/tests/src/python/test_qgsserver_accesscontrol.py +++ b/tests/src/python/test_qgsserver_accesscontrol.py @@ -980,7 +980,7 @@ class TestQgsServerAccessControl(unittest.TestCase): test we reuse the projectsubsetstring reference images as we are using filter requests to set the same filter " pkuid in (7,8) " as the project subsetstring uses for its test. """ - query_string = "&".join(["%s=%s" % i for i in list({ + query_string = "&".join(["%s=%s" % i for i in { "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", @@ -993,12 +993,12 @@ class TestQgsServerAccessControl(unittest.TestCase): "HEIGHT": "500", "WIDTH": "500", "SRS": "EPSG:3857" - }.items())]) + }.items()]) response, headers = self._get_fullaccess(query_string) self._img_diff_error(response, headers, "WMS_GetMap_projectsubstring") - query_string = "&".join(["%s=%s" % i for i in list({ + query_string = "&".join(["%s=%s" % i for i in { "MAP": urllib.parse.quote(self.projectPath), "SERVICE": "WMS", "VERSION": "1.1.1", @@ -1011,11 +1011,32 @@ class TestQgsServerAccessControl(unittest.TestCase): "HEIGHT": "500", "WIDTH": "500", "SRS": "EPSG:3857" - }.items())]) + }.items()]) response, headers = self._get_restricted(query_string) self._img_diff_error(response, headers, "Restricted_WMS_GetMap_projectsubstring") + filter = "pkuid7" \ + "pkuid8" \ + "" + query_string = "&".join(["%s=%s" % i for i in { + "MAP": urllib.parse.quote(self.projectPath), + "SERVICE": "WMS", + "VERSION": "1.1.1", + "REQUEST": "GetMap", + "LAYERS": "Hello_Filter_SubsetString", + "FILTER": filter, + "STYLES": "", + "FORMAT": "image/png", + "BBOX": "-16817707,-6318936.5,5696513,16195283.5", + "HEIGHT": "500", + "WIDTH": "500", + "SRS": "EPSG:3857" + }.items()]) + + response, headers = self._get_restricted(query_string) + self._img_diff_error(response, headers, "Restricted_WMS_GetMap_projectsubstring_OGC") + def test_wms_getmap_projectsubsetstring(self): """ test that project set layer subsetStrings are honored""" query_string = "&".join(["%s=%s" % i for i in list({ diff --git a/tests/src/python/test_qgsserver_security.py b/tests/src/python/test_qgsserver_security.py index e0410f0463e..359b969c6cf 100644 --- a/tests/src/python/test_qgsserver_security.py +++ b/tests/src/python/test_qgsserver_security.py @@ -283,6 +283,37 @@ class TestQgsServerSecurity(QgsServerTestBase): self.handle_request_wfs_getfeature_filter(filter_xml) self.assertTrue(self.is_point_table_still_exist()) + def test_wms_getmap_filter_stacked(self): + """ + The aim is to execute some staked queries within the 'Literal' + and 'PropertyName' field. Here the 'drop' function is used but it + could be done with create, insert, ... + + But due to the implementation, these filters quoted before being sent to the DB. + + It's typically the kind of SQL injection which has been fixed in + mapserver several years ago: + https://trac.osgeo.org/mapserver/ticket/3874 + """ + + # ogc:Literal / ogc:PropertyIsEqualTo + literal = "4')); drop table point --" + filter_xml = "pkuid{0}".format(literal) + self.handle_request_wms_getmap(filter=filter_xml) + self.assertTrue(self.is_point_table_still_exist()) + + # ogc:Literal / ogc:PropertyIsLike + literal = "4')); drop table point --" + filter_xml = "pkuid{0}".format(literal) + self.handle_request_wms_getmap(filter=filter_xml) + self.assertTrue(self.is_point_table_still_exist()) + + # ogc:PropertyName / ogc:PropertyIsLike + propname = "name = 'a')); drop table point --" + filter_xml = "{0}4".format(propname) + self.handle_request_wms_getmap(filter=filter_xml) + self.assertTrue(self.is_point_table_still_exist()) + def test_wms_getmap_sld_stacked(self): """ The aim is to execute some staked queries within the 'Literal' @@ -297,7 +328,7 @@ class TestQgsServerSecurity(QgsServerTestBase): literal = "4')); drop table point --" sld = " point point Single symbol pkuid {0} circle 5e86a10000007".format(literal) - self.handle_request_wms_getmap(sld) + self.handle_request_wms_getmap(sld=sld) self.assertTrue(self.is_point_table_still_exist()) def check_service_exception_report(self, d): @@ -311,7 +342,7 @@ class TestQgsServerSecurity(QgsServerTestBase): return False def handle_request_wfs_getfeature_filter(self, filter_xml): - qs = "?" + "&".join(["%s=%s" % i for i in list({ + qs = "?" + "&".join(["%s=%s" % i for i in { "MAP": urllib.parse.quote(self.project), "SERVICE": "WFS", "VERSION": "1.1.1", @@ -319,12 +350,12 @@ class TestQgsServerSecurity(QgsServerTestBase): "TYPENAME": "point", "STYLES": "", "CRS": "EPSG:32613", - "FILTER": filter_xml}.items())]) + "FILTER": filter_xml}.items()]) return self._execute_request(qs) def handle_request_wms_getfeatureinfo(self, filter_sql): - qs = "?" + "&".join(["%s=%s" % i for i in list({ + qs = "?" + "&".join(["%s=%s" % i for i in { "MAP": urllib.parse.quote(self.project), "SERVICE": "WMS", "VERSION": "1.1.1", @@ -337,12 +368,12 @@ class TestQgsServerSecurity(QgsServerTestBase): "WIDTH": "500", "BBOX": "606171,4822867,612834,4827375", "CRS": "EPSG:32613", - "FILTER": filter_sql}.items())]) + "FILTER": filter_sql}.items()]) return self._result(self._execute_request(qs)) - def handle_request_wms_getmap(self, sld): - qs = "?" + "&".join(["%s=%s" % i for i in list({ + def handle_request_wms_getmap(self, sld=None, filter=None): + params = { "MAP": urllib.parse.quote(self.project), "SERVICE": "WMS", "VERSION": "1.0.0", @@ -354,8 +385,13 @@ class TestQgsServerSecurity(QgsServerTestBase): "HEIGHT": "500", "WIDTH": "500", "BBOX": "606171,4822867,612834,4827375", - "CRS": "EPSG:32613", - "SLD": sld}.items())]) + "CRS": "EPSG:32613" + } + if sld is not None: + params["SLD"] = sld + if filter is not None: + params["FILTER"] = urllib.parse.quote(filter) + qs = "?" + "&".join(["%s=%s" % i for i in params.items()]) return self._result(self._execute_request(qs)) diff --git a/tests/src/python/test_qgsserver_wms_getmap.py b/tests/src/python/test_qgsserver_wms_getmap.py index 781ad83c60b..7779fe814ee 100644 --- a/tests/src/python/test_qgsserver_wms_getmap.py +++ b/tests/src/python/test_qgsserver_wms_getmap.py @@ -697,6 +697,27 @@ class TestQgsServerWMSGetMap(QgsServerTestBase): r, h = self._result(self._execute_request(qs)) self._img_diff_error(r, h, "WMS_GetMap_Filter3") + def test_wms_getmap_filter_ogc(self): + filter = "name" + \ + "eurasia" + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectPath), + "SERVICE": "WMS", + "VERSION": "1.1.1", + "REQUEST": "GetMap", + "LAYERS": "Country,Hello", + "STYLES": "", + "FORMAT": "image/png", + "BBOX": "-16817707,-4710778,5696513,14587125", + "HEIGHT": "500", + "WIDTH": "500", + "CRS": "EPSG:3857", + "FILTER": filter + }.items())]) + + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMS_GetMap_Filter_OGC") + def test_wms_getmap_selection(self): qs = "?" + "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Filter_OGC/WMS_GetMap_Filter_OGC.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Filter_OGC/WMS_GetMap_Filter_OGC.png new file mode 100644 index 00000000000..12da8bb0ee5 Binary files /dev/null and b/tests/testdata/control_images/qgis_server/WMS_GetMap_Filter_OGC/WMS_GetMap_Filter_OGC.png differ diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Filter_OGC/WMS_GetMap_Filter_OGC_mask.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Filter_OGC/WMS_GetMap_Filter_OGC_mask.png new file mode 100644 index 00000000000..9fa1281fe13 Binary files /dev/null and b/tests/testdata/control_images/qgis_server/WMS_GetMap_Filter_OGC/WMS_GetMap_Filter_OGC_mask.png differ diff --git a/tests/testdata/control_images/qgis_server_accesscontrol/Restricted_WMS_GetMap_projectsubstring_OGC/Restricted_WMS_GetMap_projectsubstring_OGC.png b/tests/testdata/control_images/qgis_server_accesscontrol/Restricted_WMS_GetMap_projectsubstring_OGC/Restricted_WMS_GetMap_projectsubstring_OGC.png new file mode 100644 index 00000000000..849c64ca976 Binary files /dev/null and b/tests/testdata/control_images/qgis_server_accesscontrol/Restricted_WMS_GetMap_projectsubstring_OGC/Restricted_WMS_GetMap_projectsubstring_OGC.png differ diff --git a/tests/testdata/control_images/qgis_server_accesscontrol/Restricted_WMS_GetMap_projectsubstring_OGC/Restricted_WMS_GetMap_projectsubstring_OGC_mask.png b/tests/testdata/control_images/qgis_server_accesscontrol/Restricted_WMS_GetMap_projectsubstring_OGC/Restricted_WMS_GetMap_projectsubstring_OGC_mask.png new file mode 100644 index 00000000000..9cb756fd327 Binary files /dev/null and b/tests/testdata/control_images/qgis_server_accesscontrol/Restricted_WMS_GetMap_projectsubstring_OGC/Restricted_WMS_GetMap_projectsubstring_OGC_mask.png differ