[server] OAPIF URL root path configuration option

Make the OAPIF service root path configurable through a new server setting
QGIS_SERVER_API_WFS3_ROOT_PATH

Change the default from “/wfs3” to “/oapif” only since QGIS 4.

Creation of a new test to prove the functionality of the new setting.

Funded by: QGIS Anwendergruppe Deutschland
This commit is contained in:
Alessandro Pasotti 2025-09-05 11:30:44 +02:00
parent 3a23b2722f
commit 892547803e
10 changed files with 132 additions and 6 deletions

View File

@ -20,6 +20,7 @@ QgsServerSettingsEnv.QGIS_SERVER_WMS_MAX_HEIGHT = QgsServerSettingsEnv.EnvVar.QG
QgsServerSettingsEnv.QGIS_SERVER_WMS_MAX_WIDTH = QgsServerSettingsEnv.EnvVar.QGIS_SERVER_WMS_MAX_WIDTH
QgsServerSettingsEnv.QGIS_SERVER_API_RESOURCES_DIRECTORY = QgsServerSettingsEnv.EnvVar.QGIS_SERVER_API_RESOURCES_DIRECTORY
QgsServerSettingsEnv.QGIS_SERVER_API_WFS3_MAX_LIMIT = QgsServerSettingsEnv.EnvVar.QGIS_SERVER_API_WFS3_MAX_LIMIT
QgsServerSettingsEnv.QGIS_SERVER_API_WFS3_ROOT_PATH = QgsServerSettingsEnv.EnvVar.QGIS_SERVER_API_WFS3_ROOT_PATH
QgsServerSettingsEnv.QGIS_SERVER_TRUST_LAYER_METADATA = QgsServerSettingsEnv.EnvVar.QGIS_SERVER_TRUST_LAYER_METADATA
QgsServerSettingsEnv.QGIS_SERVER_FORCE_READONLY_LAYERS = QgsServerSettingsEnv.EnvVar.QGIS_SERVER_FORCE_READONLY_LAYERS
QgsServerSettingsEnv.QGIS_SERVER_DISABLE_GETPRINT = QgsServerSettingsEnv.EnvVar.QGIS_SERVER_DISABLE_GETPRINT

View File

@ -48,6 +48,7 @@ configuration.
QGIS_SERVER_WMS_MAX_WIDTH,
QGIS_SERVER_API_RESOURCES_DIRECTORY,
QGIS_SERVER_API_WFS3_MAX_LIMIT,
QGIS_SERVER_API_WFS3_ROOT_PATH,
QGIS_SERVER_TRUST_LAYER_METADATA,
QGIS_SERVER_FORCE_READONLY_LAYERS,
QGIS_SERVER_DISABLE_GETPRINT,
@ -271,6 +272,20 @@ The default value is 10000, this value can be changed by setting the
environment variable QGIS_SERVER_API_WFS3_MAX_LIMIT.
.. versionadded:: 3.10
%End
QString apiWfs3RootPath() const;
%Docstring
Returns the server-wide root path for OAPIF (WFS3) service API.
The default value is "/wfs3", this value can be changed by setting the
environment variable QGIS_SERVER_API_WFS3_ROOT_PATH.
.. note::
The default will be changed to "/oapif" for QGIS 4.
.. versionadded:: 3.44.3
%End
bool ignoreBadLayers() const;

View File

@ -48,6 +48,7 @@ configuration.
QGIS_SERVER_WMS_MAX_WIDTH,
QGIS_SERVER_API_RESOURCES_DIRECTORY,
QGIS_SERVER_API_WFS3_MAX_LIMIT,
QGIS_SERVER_API_WFS3_ROOT_PATH,
QGIS_SERVER_TRUST_LAYER_METADATA,
QGIS_SERVER_FORCE_READONLY_LAYERS,
QGIS_SERVER_DISABLE_GETPRINT,
@ -271,6 +272,20 @@ The default value is 10000, this value can be changed by setting the
environment variable QGIS_SERVER_API_WFS3_MAX_LIMIT.
.. versionadded:: 3.10
%End
QString apiWfs3RootPath() const;
%Docstring
Returns the server-wide root path for OAPIF (WFS3) service API.
The default value is "/wfs3", this value can be changed by setting the
environment variable QGIS_SERVER_API_WFS3_ROOT_PATH.
.. note::
The default will be changed to "/oapif" for QGIS 4.
.. versionadded:: 3.44.3
%End
bool ignoreBadLayers() const;

View File

@ -116,6 +116,16 @@ void QgsServerSettings::initSettings()
mSettings[sApiWfs3MaxLimit.envVar] = sApiWfs3MaxLimit;
// API WFS3 root path
// TODO: remove when QGIS 4 is released
#if _QGIS_VERSION_INT > 40000
const Setting sApiWfs3RootPath = { QgsServerSettingsEnv::QGIS_SERVER_API_WFS3_ROOT_PATH, QgsServerSettingsEnv::DEFAULT_VALUE, QStringLiteral( "Root path for the OAPIF (WFS3) API" ), QStringLiteral( "/qgis/server_api_wfs3_root_path" ), QMetaType::Type::QString, QVariant( "/oapif" ), QVariant() };
#else
const Setting sApiWfs3RootPath = { QgsServerSettingsEnv::QGIS_SERVER_API_WFS3_ROOT_PATH, QgsServerSettingsEnv::DEFAULT_VALUE, QStringLiteral( "Root path for the OAPIF (WFS3) API" ), QStringLiteral( "/qgis/server_api_wfs3_root_path" ), QMetaType::Type::QString, QVariant( "/wfs3" ), QVariant() };
#endif
mSettings[sApiWfs3RootPath.envVar] = sApiWfs3RootPath;
// projects directory for landing page service
const Setting sProjectsDirectories = { QgsServerSettingsEnv::QGIS_SERVER_LANDING_PAGE_PROJECTS_DIRECTORIES, QgsServerSettingsEnv::DEFAULT_VALUE, QStringLiteral( "Directories used by the landing page service to find .qgs and .qgz projects" ), QStringLiteral( "/qgis/server_projects_directories" ), QMetaType::Type::QString, QVariant( "" ), QVariant() };
@ -411,6 +421,11 @@ qlonglong QgsServerSettings::apiWfs3MaxLimit() const
return value( QgsServerSettingsEnv::QGIS_SERVER_API_WFS3_MAX_LIMIT ).toLongLong();
}
QString QgsServerSettings::apiWfs3RootPath() const
{
return value( QgsServerSettingsEnv::QGIS_SERVER_API_WFS3_ROOT_PATH ).toString();
}
bool QgsServerSettings::ignoreBadLayers() const
{
return value( QgsServerSettingsEnv::QGIS_SERVER_IGNORE_BAD_LAYERS ).toBool();

View File

@ -66,6 +66,7 @@ class SERVER_EXPORT QgsServerSettingsEnv : public QObject
QGIS_SERVER_WMS_MAX_WIDTH, //!< Maximum width for a WMS request. The most conservative between this and the project one is used \since QGIS 3.6.2
QGIS_SERVER_API_RESOURCES_DIRECTORY, //!< Base directory where HTML templates and static assets (e.g. images, js and css files) are searched for \since QGIS 3.10
QGIS_SERVER_API_WFS3_MAX_LIMIT, //!< Maximum value for "limit" in a features request, defaults to 10000 \since QGIS 3.10
QGIS_SERVER_API_WFS3_ROOT_PATH, //!< Root path for OAPIF (WFS3) service API, default value is "/wfs3" \note Default will be changed to "/oapif" for QGIS 4. \since QGIS 3.44.3
QGIS_SERVER_TRUST_LAYER_METADATA, //!< Trust layer metadata. Improves project read time. \since QGIS 3.16
QGIS_SERVER_FORCE_READONLY_LAYERS, //!< Force to open layers in read-only mode. \since QGIS 3.28
QGIS_SERVER_DISABLE_GETPRINT, //!< Disabled WMS GetPrint request and don't load layouts. Improves project read time. \since QGIS 3.16
@ -262,6 +263,16 @@ class SERVER_EXPORT QgsServerSettings
*/
qlonglong apiWfs3MaxLimit() const;
/**
* Returns the server-wide root path for OAPIF (WFS3) service API.
*
* The default value is "/wfs3", this value can be changed by setting the environment
* variable QGIS_SERVER_API_WFS3_ROOT_PATH.
* \note The default will be changed to "/oapif" for QGIS 4.
* \since QGIS 3.44.3
*/
QString apiWfs3RootPath() const;
/**
* Returns TRUE if the bad layers are ignored and FALSE when the presence of a
* bad layers invalidates the whole project making it unavailable.

View File

@ -353,7 +353,7 @@ bool QgsServiceRegistry::registerApi( QgsServerApi *api )
return false;
}
QgsMessageLog::logMessage( QStringLiteral( "Adding API %1 %2" ).arg( name, version ), QString(), Qgis::MessageLevel::Info );
QgsMessageLog::logMessage( QStringLiteral( "Adding API %1 %2 - root path: %3" ).arg( name, version, api->rootPath() ), QString(), Qgis::MessageLevel::Info );
mApis.insert( key, std::shared_ptr<QgsServerApi>( api ) );
// Check the default version

View File

@ -23,7 +23,7 @@
/**
* \ingroup server
* \class QgsWfsModule
* \brief Module specialized for WFS3 service
* \brief Module specialized for OAPIF (WFS3) service
* \since QGIS 3.10
*/
class QgsWfs3Module : public QgsServiceModule
@ -31,7 +31,17 @@ class QgsWfs3Module : public QgsServiceModule
public:
void registerSelf( QgsServiceRegistry &registry, QgsServerInterface *serverIface ) override
{
QgsServerOgcApi *wfs3Api = new QgsServerOgcApi { serverIface, QStringLiteral( "/wfs3" ), QStringLiteral( "OGC WFS3 (Draft)" ), QStringLiteral( "1.0.0" ) };
// TODO: remove when QGIS 4 is released
#if _QGIS_VERSION_INT >= 40000
QString rootPath = QStringLiteral( "/oapif" );
#else
QString rootPath = QStringLiteral( "/wfs3" );
#endif
if ( serverIface && serverIface->serverSettings() && !serverIface->serverSettings()->apiWfs3RootPath().isEmpty() )
{
rootPath = serverIface->serverSettings()->apiWfs3RootPath();
}
std::unique_ptr<QgsServerOgcApi> wfs3Api = std::make_unique<QgsServerOgcApi>( serverIface, rootPath, QStringLiteral( "OAPIF" ), QStringLiteral( "1.0.0" ) );
// Register handlers
wfs3Api->registerHandler<QgsWfs3CollectionsItemsHandler>();
wfs3Api->registerHandler<QgsWfs3CollectionsFeatureHandler>();
@ -40,11 +50,11 @@ class QgsWfs3Module : public QgsServiceModule
wfs3Api->registerHandler<QgsWfs3ConformanceHandler>();
wfs3Api->registerHandler<QgsServerStaticHandler>();
// API handler must access to the whole API
wfs3Api->registerHandler<QgsWfs3APIHandler>( wfs3Api );
wfs3Api->registerHandler<QgsWfs3APIHandler>( wfs3Api.get() );
wfs3Api->registerHandler<QgsWfs3LandingPageHandler>();
// Register API
registry.registerApi( wfs3Api );
registry.registerApi( wfs3Api.release() );
}
};

View File

@ -213,6 +213,17 @@ QgsFields QgsWfs3AbstractItemsHandler::publishedFields( const QgsVectorLayer *vL
return publishedFields;
}
const QString QgsWfs3AbstractItemsHandler::templatePath( const QgsServerApiContext &context ) const
{
// resources/server/api + /ogc/templates/ + operationId + .html
QString path { context.serverInterface()->serverSettings()->apiResourcesDirectory() };
path += QLatin1String( "/ogc/templates/wfs3" );
path += '/';
path += QString::fromStdString( operationId() );
path += QLatin1String( ".html" );
return path;
}
QgsWfs3LandingPageHandler::QgsWfs3LandingPageHandler()
{
}

View File

@ -59,6 +59,16 @@ class QgsWfs3AbstractItemsHandler : public QgsServerOgcApiHandler
* \return QgsFields list with filters applied
*/
QgsFields publishedFields( const QgsVectorLayer *layer, const QgsServerApiContext &context ) const;
/**
* Returns the HTML template path for the handler in the given \a context
*
* The template path is calculated from QgsServerSettings's apiResourcesDirectory() as follow:
* apiResourcesDirectory() + "/ogc/templates/wfs3/" + operationId + ".html"
* e.g. for an handler with operationId "collectionItems", the path
* will be apiResourcesDirectory() + "/ogc/templates/wfs3/collectionItems.html"
*/
const QString templatePath( const QgsServerApiContext &context ) const override;
};
/**

View File

@ -30,6 +30,7 @@ from qgis.core import (
QgsProject,
QgsVectorLayer,
QgsVectorLayerServerProperties,
QgsApplication,
)
from qgis.PyQt import QtCore
from qgis.server import (
@ -49,6 +50,7 @@ from qgis.server import (
from qgis.testing import unittest
from test_qgsserver import QgsServerTestBase
from utilities import unitTestDataPath
from contextlib import contextmanager
class QgsServerAPIUtilsTest(QgsServerTestBase):
@ -283,7 +285,9 @@ class QgsServerAPITestBase(QgsServerTestBase):
)
return headers_content + "\n" + json_content
def compareApi(self, request, project, reference_file, subdir="api"):
def compareApi(
self, request, project, reference_file, subdir="api", replace_map=None
):
response = QgsBufferServerResponse()
# Add json to accept it reference_file is JSON
if reference_file.endswith(".json"):
@ -294,6 +298,11 @@ class QgsServerAPITestBase(QgsServerTestBase):
if reference_file.endswith("html")
else self.dump(response)
)
if replace_map:
for k, v in replace_map.items():
result = result.replace(k, v)
path = os.path.join(self.temporary_path, "qgis_server", subdir, reference_file)
if self.regeregenerate_api_reference:
# Try to change timestamp
@ -374,6 +383,28 @@ class RestrictedLayerAccessControl(QgsAccessControlFilter):
class QgsServerAPITest(QgsServerAPITestBase):
"""QGIS API server tests"""
# Set env context manager
@contextmanager
def set_env(self, **environ):
"""Context manager to set/unset environment variables"""
old_environ = dict(os.environ)
os.environ.update(environ)
iface = self.server.serverInterface()
iface.reloadSettings()
iface.serviceRegistry().cleanUp()
iface.serviceRegistry().init(QgsApplication.libexecPath() + "server", iface)
try:
yield
finally:
os.environ.clear()
os.environ.update(old_environ)
iface.reloadSettings()
iface.serviceRegistry().cleanUp()
iface.serviceRegistry().init(QgsApplication.libexecPath() + "server", iface)
def test_api(self):
"""Test API registering"""
@ -522,6 +553,13 @@ class QgsServerAPITest(QgsServerAPITestBase):
)
self.compareApi(request, project, "test_wfs3_api_project.json")
with self.set_env(QGIS_SERVER_API_WFS3_ROOT_PATH="/custom_rootpath"):
iface = self.server.serverInterface()
registry = iface.serviceRegistry()
api = registry.getApi("OAPIF")
self.assertIsNotNone(api)
self.assertEqual(api.rootPath(), "/custom_rootpath")
def test_wfs3_api_permissions(self):
"""Test the API with different permissions on a layer"""