mirror of
				https://github.com/qgis/QGIS.git
				synced 2025-11-04 00:04:25 -05:00 
			
		
		
		
	Server: add QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS settings var
This new feature allows to specify extra tokens allowed for WMS FILTER definition. The current list of accepted tokens is rather small and this setting will allow the sysadmins to enlarge the list of allowed tokens.
This commit is contained in:
		
							parent
							
								
									1d9495b8a9
								
							
						
					
					
						commit
						2beed8345c
					
				@ -136,6 +136,13 @@ Returns the service registry
 | 
			
		||||
%End
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    virtual void reloadSettings() = 0;
 | 
			
		||||
%Docstring
 | 
			
		||||
Reloads the server settings re-reading the configuration.
 | 
			
		||||
 | 
			
		||||
.. versionadded:: 3.28
 | 
			
		||||
%End
 | 
			
		||||
 | 
			
		||||
  private:
 | 
			
		||||
    QgsServerInterface();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -61,6 +61,7 @@ Provides some enum describing the environment currently supported for configurat
 | 
			
		||||
      QGIS_SERVER_LANDING_PAGE_PREFIX,
 | 
			
		||||
      QGIS_SERVER_PROJECT_CACHE_CHECK_INTERVAL,
 | 
			
		||||
      QGIS_SERVER_PROJECT_CACHE_STRATEGY,
 | 
			
		||||
      QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -317,6 +318,16 @@ Possible values are:
 | 
			
		||||
- 'off': Disable completely internal project's cache handling
 | 
			
		||||
 | 
			
		||||
.. versionadded:: 3.26
 | 
			
		||||
%End
 | 
			
		||||
 | 
			
		||||
    QStringList allowedExtraSqlTokens() const;
 | 
			
		||||
%Docstring
 | 
			
		||||
Returns the list of strings that represent the allowed extra SQL tokens
 | 
			
		||||
accepted as componenst of a feature filter.
 | 
			
		||||
The default value is an empty string, the value can be changed by setting the environment
 | 
			
		||||
variable QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS.
 | 
			
		||||
 | 
			
		||||
.. versionadded:: 3.28
 | 
			
		||||
%End
 | 
			
		||||
 | 
			
		||||
    static QString name( QgsServerSettingsEnv::EnvVar env );
 | 
			
		||||
 | 
			
		||||
@ -170,6 +170,13 @@ class SERVER_EXPORT QgsServerInterface
 | 
			
		||||
     */
 | 
			
		||||
    virtual QgsServerSettings *serverSettings() = 0 SIP_SKIP;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reloads the server settings re-reading the configuration.
 | 
			
		||||
     *
 | 
			
		||||
     * \since QGIS 3.28
 | 
			
		||||
     */
 | 
			
		||||
    virtual void reloadSettings() = 0;
 | 
			
		||||
 | 
			
		||||
  private:
 | 
			
		||||
#ifdef SIP_RUN
 | 
			
		||||
    QgsServerInterface();
 | 
			
		||||
 | 
			
		||||
@ -118,3 +118,8 @@ QgsServerSettings *QgsServerInterfaceImpl::serverSettings()
 | 
			
		||||
{
 | 
			
		||||
  return mServerSettings;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QgsServerInterfaceImpl::reloadSettings()
 | 
			
		||||
{
 | 
			
		||||
  mServerSettings->load();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,8 @@ class SERVER_EXPORT QgsServerInterfaceImpl : public QgsServerInterface
 | 
			
		||||
 | 
			
		||||
    QgsServerSettings *serverSettings() override;
 | 
			
		||||
 | 
			
		||||
    void reloadSettings() override;
 | 
			
		||||
 | 
			
		||||
  private:
 | 
			
		||||
 | 
			
		||||
    QString mConfigFilePath;
 | 
			
		||||
@ -97,6 +99,7 @@ class SERVER_EXPORT QgsServerInterfaceImpl : public QgsServerInterface
 | 
			
		||||
    QgsRequestHandler *mRequestHandler = nullptr;
 | 
			
		||||
    QgsServiceRegistry *mServiceRegistry = nullptr;
 | 
			
		||||
    QgsServerSettings *mServerSettings = nullptr;
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif // QGSSERVERINTERFACEIMPL_H
 | 
			
		||||
 | 
			
		||||
@ -357,6 +357,16 @@ void QgsServerSettings::initSettings()
 | 
			
		||||
                                        };
 | 
			
		||||
  mSettings[ sProjectCacheStrategy.envVar ] = sProjectCacheStrategy;
 | 
			
		||||
 | 
			
		||||
  const Setting sAllowedExtraSqlTokens = { QgsServerSettingsEnv::QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS,
 | 
			
		||||
                                           QgsServerSettingsEnv::DEFAULT_VALUE,
 | 
			
		||||
                                           QStringLiteral( "List of comma separated SQL tokens to be added to the list of allowed tokens that the services accespt when filtering features" ),
 | 
			
		||||
                                           QStringLiteral( "/qgis/server_allowed_extra_sql_tokens" ),
 | 
			
		||||
                                           QVariant::String,
 | 
			
		||||
                                           QVariant( "" ),
 | 
			
		||||
                                           QVariant()
 | 
			
		||||
                                         };
 | 
			
		||||
  mSettings[ sAllowedExtraSqlTokens.envVar ] = sAllowedExtraSqlTokens;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QgsServerSettings::load()
 | 
			
		||||
@ -658,3 +668,8 @@ QString QgsServerSettings::projectCacheStrategy() const
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QStringList QgsServerSettings::allowedExtraSqlTokens() const
 | 
			
		||||
{
 | 
			
		||||
  return value( QgsServerSettingsEnv::QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS ).toString().split( ',' );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -79,6 +79,7 @@ class SERVER_EXPORT QgsServerSettingsEnv : public QObject
 | 
			
		||||
      QGIS_SERVER_LANDING_PAGE_PREFIX, //! Prefix of the path component of the landing page base URL, default is empty (since QGIS 3.20).
 | 
			
		||||
      QGIS_SERVER_PROJECT_CACHE_CHECK_INTERVAL, //! Set the interval for cache invalidation strategy 'interval', default to 0 which select the legacy File system watcher  (since QGIS 3.26).
 | 
			
		||||
      QGIS_SERVER_PROJECT_CACHE_STRATEGY, //! Set the project cache strategy. Possible values are 'filesystem', 'periodic' or 'off' (since QGIS 3.26).
 | 
			
		||||
      QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS, //! Adds these tokens to the list of allowed tokens that the services accespt when filtering features (since QGIS 3.28).
 | 
			
		||||
    };
 | 
			
		||||
    Q_ENUM( EnvVar )
 | 
			
		||||
};
 | 
			
		||||
@ -317,6 +318,16 @@ class SERVER_EXPORT QgsServerSettings
 | 
			
		||||
     */
 | 
			
		||||
    QString projectCacheStrategy() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the list of strings that represent the allowed extra SQL tokens
 | 
			
		||||
     * accepted as componenst of a feature filter.
 | 
			
		||||
     * The default value is an empty string, the value can be changed by setting the environment
 | 
			
		||||
     * variable QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS.
 | 
			
		||||
     *
 | 
			
		||||
     * \since QGIS 3.28
 | 
			
		||||
     */
 | 
			
		||||
    QStringList allowedExtraSqlTokens() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the string representation of a setting.
 | 
			
		||||
     * \since QGIS 3.16
 | 
			
		||||
 | 
			
		||||
@ -2172,7 +2172,8 @@ namespace QgsWms
 | 
			
		||||
           || tokenIt->compare( QLatin1String( "LIKE" ), Qt::CaseInsensitive ) == 0
 | 
			
		||||
           || tokenIt->compare( QLatin1String( "ILIKE" ), Qt::CaseInsensitive ) == 0
 | 
			
		||||
           || tokenIt->compare( QLatin1String( "DMETAPHONE" ), Qt::CaseInsensitive ) == 0
 | 
			
		||||
           || tokenIt->compare( QLatin1String( "SOUNDEX" ), Qt::CaseInsensitive ) == 0 )
 | 
			
		||||
           || tokenIt->compare( QLatin1String( "SOUNDEX" ), Qt::CaseInsensitive ) == 0
 | 
			
		||||
           || mContext.settings().allowedExtraSqlTokens().contains( *tokenIt, Qt::CaseSensitivity::CaseInsensitive ) )
 | 
			
		||||
      {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
@ -3127,6 +3128,13 @@ namespace QgsWms
 | 
			
		||||
 | 
			
		||||
  void QgsRenderer::setLayerFilter( QgsMapLayer *layer, const QList<QgsWmsParametersFilter> &filters )
 | 
			
		||||
  {
 | 
			
		||||
 | 
			
		||||
    // quick exit if no filters are set
 | 
			
		||||
    if ( filters.isEmpty() )
 | 
			
		||||
    {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ( layer->type() == QgsMapLayerType::VectorLayer )
 | 
			
		||||
    {
 | 
			
		||||
      QgsVectorLayer *filteredLayer = qobject_cast<QgsVectorLayer *>( layer );
 | 
			
		||||
@ -3161,9 +3169,11 @@ namespace QgsWms
 | 
			
		||||
                                        " 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 "
 | 
			
		||||
                                        " IS,NOT,NULL,AND,OR,IN,=,<,>=,>,>=,!=,',',(,),DMETAPHONE,SOUNDEX."
 | 
			
		||||
                                        " IS,NOT,NULL,AND,OR,IN,=,<,>=,>,>=,!=,',',(,),DMETAPHONE,SOUNDEX%2."
 | 
			
		||||
                                        " Not allowed are semicolons in the filter expression." ).arg(
 | 
			
		||||
                                          filter.mFilter ) );
 | 
			
		||||
                                          filter.mFilter, mContext.settings().allowedExtraSqlTokens().isEmpty() ?
 | 
			
		||||
                                          QString() :
 | 
			
		||||
                                          mContext.settings().allowedExtraSqlTokens().join( ',' ).prepend( ',' ) ) );
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          QString newSubsetString = filter.mFilter;
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ import osgeo.gdal  # NOQA
 | 
			
		||||
 | 
			
		||||
from test_qgsserver_wms import TestQgsServerWMSTestBase
 | 
			
		||||
from qgis.core import QgsProject
 | 
			
		||||
from qgis.server import QgsBufferServerRequest, QgsBufferServerResponse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
 | 
			
		||||
@ -41,6 +42,10 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
 | 
			
		||||
 | 
			
		||||
    # regenerate_reference = True
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        super().tearDown()
 | 
			
		||||
        os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', '')
 | 
			
		||||
 | 
			
		||||
    def testGetFeatureInfo(self):
 | 
			
		||||
        # Test getfeatureinfo response xml
 | 
			
		||||
        self.wms_request_compare('GetFeatureInfo',
 | 
			
		||||
@ -861,6 +866,89 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(response_body.decode('utf8'), '<ServiceExceptionReport xmlns="http://www.opengis.net/ogc" version="1.3.0">\n <ServiceException code="InvalidParameterValue">Filter not valid for layer testlayer èé: check the filter syntax and the field names.</ServiceException>\n</ServiceExceptionReport>\n')
 | 
			
		||||
 | 
			
		||||
    def testGetFeatureInfoFilterAllowedExtraTokens(self):
 | 
			
		||||
        """Test GetFeatureInfo with forbidden and extra tokens
 | 
			
		||||
        set by QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS
 | 
			
		||||
        """
 | 
			
		||||
        project_path = self.testdata_path + "test_project_values.qgz"
 | 
			
		||||
        project = QgsProject()
 | 
			
		||||
        self.assertTrue(project.read(project_path))
 | 
			
		||||
 | 
			
		||||
        req_params = {
 | 
			
		||||
            'SERVICE': 'WMS',
 | 
			
		||||
            'REQUEST': 'GetFeatureInfo',
 | 
			
		||||
            'VERSION': '1.3.0',
 | 
			
		||||
            'LAYERS': 'layer4',
 | 
			
		||||
            'STYLES': '',
 | 
			
		||||
            'INFO_FORMAT': r'application%2Fjson',
 | 
			
		||||
            'WIDTH': '926',
 | 
			
		||||
            'HEIGHT': '787',
 | 
			
		||||
            'SRS': r'EPSG%3A4326',
 | 
			
		||||
            'BBOX': '912217,5605059,914099,5606652',
 | 
			
		||||
            'CRS': 'EPSG:3857',
 | 
			
		||||
            'FEATURE_COUNT': '10',
 | 
			
		||||
            'QUERY_LAYERS': 'layer4',
 | 
			
		||||
            'FILTER': 'layer4:"utf8nameè" != \'\'',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
 | 
			
		||||
        res = QgsBufferServerResponse()
 | 
			
		||||
        self.server.handleRequest(req, res, project)
 | 
			
		||||
        j_body = json.loads(bytes(res.body()).decode())
 | 
			
		||||
        self.assertEqual(len(j_body['features']), 3)
 | 
			
		||||
 | 
			
		||||
        req_params['FILTER'] = 'layer4:"utf8nameè" = \'three èé↓\''
 | 
			
		||||
        req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
 | 
			
		||||
        res = QgsBufferServerResponse()
 | 
			
		||||
        self.server.handleRequest(req, res, project)
 | 
			
		||||
        j_body = json.loads(bytes(res.body()).decode())
 | 
			
		||||
        self.assertEqual(len(j_body['features']), 1)
 | 
			
		||||
 | 
			
		||||
        req_params['FILTER'] = 'layer4:"utf8nameè" != \'three èé↓\''
 | 
			
		||||
        req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
 | 
			
		||||
        res = QgsBufferServerResponse()
 | 
			
		||||
        self.server.handleRequest(req, res, project)
 | 
			
		||||
        j_body = json.loads(bytes(res.body()).decode())
 | 
			
		||||
        self.assertEqual(len(j_body['features']), 2)
 | 
			
		||||
 | 
			
		||||
        # REPLACE filter
 | 
			
		||||
        req_params['FILTER'] = 'layer4:REPLACE ( "utf8nameè" , \'three\' , \'____\' ) != \'____ èé↓\''
 | 
			
		||||
        req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
 | 
			
		||||
        res = QgsBufferServerResponse()
 | 
			
		||||
        self.server.handleRequest(req, res, project)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(res.statusCode(), 403)
 | 
			
		||||
 | 
			
		||||
        os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', 'RePlAcE')
 | 
			
		||||
        self.server.serverInterface().reloadSettings()
 | 
			
		||||
 | 
			
		||||
        req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
 | 
			
		||||
        res = QgsBufferServerResponse()
 | 
			
		||||
        self.server.handleRequest(req, res, project)
 | 
			
		||||
        j_body = json.loads(bytes(res.body()).decode())
 | 
			
		||||
        self.assertEqual(len(j_body['features']), 2)
 | 
			
		||||
 | 
			
		||||
        os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', '')
 | 
			
		||||
        self.server.serverInterface().reloadSettings()
 | 
			
		||||
 | 
			
		||||
        req_params['FILTER'] = 'layer4:REPLACE ( "utf8nameè" , \'three\' , \'____\' ) != \'____ èé↓\''
 | 
			
		||||
        req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
 | 
			
		||||
        res = QgsBufferServerResponse()
 | 
			
		||||
        self.server.handleRequest(req, res, project)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(res.statusCode(), 403)
 | 
			
		||||
 | 
			
		||||
        # Multiple filters
 | 
			
		||||
        os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', 'RePlAcE,LowEr')
 | 
			
		||||
        self.server.serverInterface().reloadSettings()
 | 
			
		||||
        req_params['FILTER'] = 'layer4:LOWER ( REPLACE ( "utf8nameè" , \'three\' , \'THREE\' ) ) = \'three èé↓\''
 | 
			
		||||
 | 
			
		||||
        req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
 | 
			
		||||
        res = QgsBufferServerResponse()
 | 
			
		||||
        self.server.handleRequest(req, res, project)
 | 
			
		||||
        j_body = json.loads(bytes(res.body()).decode())
 | 
			
		||||
        self.assertEqual(len(j_body['features']), 1)
 | 
			
		||||
 | 
			
		||||
    def testGetFeatureInfoSortedByDesignerWithJoinLayer(self):
 | 
			
		||||
        """Test GetFeatureInfo resolves DRAG&DROP Designer order when use attribute form settings for GetFeatureInfo
 | 
			
		||||
        with a column from a Joined Layer when the option is checked, see https://github.com/qgis/QGIS/pull/41031
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user