Compare commits

...

17 Commits

Author SHA1 Message Date
Björn
1ae5196805
Merge 24bbdda53918b2c234f01cadd1f1b3b7c61b38ff into 74549aad26c3358101e88477d9dfa1caae013d72 2025-07-01 17:19:21 +02:00
Juergen E. Fischer
74549aad26 Reapply "Allow free naming of project properties (#60855)"
This reverts commit fb11239112adfc321b3bbacbb20da888a7a37c23.
2025-07-01 09:08:44 +00:00
Juergen E. Fischer
eac401c009 if a plugin supports qt6 it should also support QGIS4 (fixes #62359) 2025-07-01 09:07:26 +00:00
Loïc Bartoletti
1f0166d35e
Merge pull request #62365 from benoitdm-oslandia/fix/cgibin_dir
fix: update default cgi-bin installation paths according to OS
2025-07-01 07:26:25 +02:00
Loïc Bartoletti
ada589bb1d
Merge pull request #62052 from martin-s42/visitPointsByRegularDistance-segfault-fix
fix segfault due to calling qgsDoubleNear with tollerance that can ne…
2025-07-01 06:59:03 +02:00
bdm-oslandia
b1c8ef3265 fixup! fix: update default cgi-bin installation paths according to OS 2025-06-27 09:35:48 +02:00
bdm-oslandia
f4cf09d4b0 fixup! fix: update default cgi-bin installation paths according to OS 2025-06-25 08:29:52 +02:00
bdm-oslandia
9db58e3726 fixup! fix: update default cgi-bin installation paths according to OS 2025-06-24 17:37:57 +02:00
bdm-oslandia
551aa20f20 fix: update default cgi-bin installation paths according to OS 2025-06-23 10:44:49 +02:00
pathmapper
24bbdda539 Add tests for explicitly enabling and disabling maptips 2025-06-12 12:50:30 +02:00
pathmapper
c3641ecdad Fix test 2025-06-09 10:37:50 +02:00
pathmapper
a36f64edba Update option description in UI 2025-06-09 10:21:08 +02:00
pathmapper
7181b40dea Add check for empty maptips to the project validator 2025-06-09 10:15:16 +02:00
pathmapper
37b2f2257d Address code review comments 2025-06-06 14:25:00 +02:00
pathmapper
30798bb462 Better wording 2025-06-06 10:24:14 +02:00
pathmapper
52a05a08bd Add project setting to enable HTML GetFeatureInfo maptip-only mode 2025-06-06 10:10:55 +02:00
Martin Siegert
a156c43f7b
fix segfault due to calling qgsDoubleNear with tollerance that can never been reached
see issue #60772: function interpolatePoint in qgsabstractprofilesurfacegenerator.cpp:157 can return a nullpointer causing a segfault because qgsDoubleNear in visitPointsByRegularDistance in qgslinestring.cpp is called with a tolerance that is too small so that qgsDoubleNear always returns false. Solution: scale epsilon by the size of the numbers to be compared.
2025-05-29 13:37:03 -07:00
24 changed files with 5148 additions and 148 deletions

View File

@ -979,7 +979,31 @@ if (WITH_CORE)
else()
# UNIX
set (DEFAULT_BIN_SUBDIR bin)
set (DEFAULT_CGIBIN_SUBDIR bin)
# From https://www.cyberciti.biz/faq/how-do-i-find-the-url-for-my-cgi-bin/
execute_process(COMMAND lsb_release -a OUTPUT_VARIABLE LSB_RELEASE_A)
if(EXISTS "/etc/fedora-release")
# in /var/www/cgi-bin
set (DEFAULT_CGIBIN_SUBDIR www/cgi-bin)
elseif (${CMAKE_HOST_SYSTEM_NAME} MATCHES "FreeBSD")
# in /usr/local/www/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR www/cgi-bin)
elseif (${CMAKE_HOST_SYSTEM_NAME} MATCHES "BSD")
# in /usr/local/libexec/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR libexec/cgi-bin)
elseif ("${LSB_RELEASE_A}" MATCHES "Ubuntu" OR "${LSB_RELEASE_A}" MATCHES "Debian" OR "${LSB_RELEASE_A}" MATCHES "Mint")
# in /usr/lib/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR lib/cgi-bin)
else()
# others: Red Hat/CentOS/Rocky/Alma Linux
# in /var/www/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR www/cgi-bin)
endif()
set (DEFAULT_LIB_SUBDIR lib${LIB_SUFFIX})
set (DEFAULT_DATA_SUBDIR share/qgis)
set (DEFAULT_LIBEXEC_SUBDIR lib${LIB_SUFFIX}/qgis)

View File

@ -4,6 +4,7 @@ QgsProjectServerValidator.LayerShortName = QgsProjectServerValidator.ValidationE
QgsProjectServerValidator.LayerEncoding = QgsProjectServerValidator.ValidationError.LayerEncoding
QgsProjectServerValidator.ProjectShortName = QgsProjectServerValidator.ValidationError.ProjectShortName
QgsProjectServerValidator.ProjectRootNameConflict = QgsProjectServerValidator.ValidationError.ProjectRootNameConflict
QgsProjectServerValidator.OnlyMaptipTrueButEmptyMaptip = QgsProjectServerValidator.ValidationError.OnlyMaptipTrueButEmptyMaptip
try:
QgsProjectServerValidator.ValidationResult.__attribute_docs__ = {'error': 'Error which occurred during the validation process.', 'identifier': 'Identifier related to the error. It can be a layer/group name.'}
QgsProjectServerValidator.ValidationResult.__annotations__ = {'error': 'QgsProjectServerValidator.ValidationError', 'identifier': 'object'}

View File

@ -33,6 +33,7 @@ project.
LayerEncoding,
ProjectShortName,
ProjectRootNameConflict,
OnlyMaptipTrueButEmptyMaptip,
};
static QString displayValidationError( QgsProjectServerValidator::ValidationError error );

View File

@ -23,6 +23,7 @@ try:
QgsServerProjectUtils.wmsInfoFormatSia2045 = staticmethod(QgsServerProjectUtils.wmsInfoFormatSia2045)
QgsServerProjectUtils.wmsFeatureInfoAddWktGeometry = staticmethod(QgsServerProjectUtils.wmsFeatureInfoAddWktGeometry)
QgsServerProjectUtils.wmsFeatureInfoUseAttributeFormSettings = staticmethod(QgsServerProjectUtils.wmsFeatureInfoUseAttributeFormSettings)
QgsServerProjectUtils.wmsHTMLFeatureInfoUseOnlyMaptip = staticmethod(QgsServerProjectUtils.wmsHTMLFeatureInfoUseOnlyMaptip)
QgsServerProjectUtils.wmsFeatureInfoSegmentizeWktGeometry = staticmethod(QgsServerProjectUtils.wmsFeatureInfoSegmentizeWktGeometry)
QgsServerProjectUtils.wmsAddLegendGroupsLegendGraphic = staticmethod(QgsServerProjectUtils.wmsAddLegendGroupsLegendGraphic)
QgsServerProjectUtils.wmsSkipNameForGroup = staticmethod(QgsServerProjectUtils.wmsSkipNameForGroup)

View File

@ -252,6 +252,20 @@ the feature info response
feature info response
%End
static bool wmsHTMLFeatureInfoUseOnlyMaptip( const QgsProject &project );
%Docstring
Returns if only the maptip should be used for HTML feature info response
so that the HTML response to the feature info request only contains the
maptip. If no maptip is set, the HTML response is empty.
:param project: the QGIS project
:return: true if only the maptip should be used for the feature info
response only
.. versionadded:: 4.0
%End
static bool wmsFeatureInfoSegmentizeWktGeometry( const QgsProject &project );
%Docstring
Returns if the geometry has to be segmentize in GetFeatureInfo request.

View File

@ -33,6 +33,7 @@ project.
LayerEncoding,
ProjectShortName,
ProjectRootNameConflict,
OnlyMaptipTrueButEmptyMaptip,
};
static QString displayValidationError( QgsProjectServerValidator::ValidationError error );

View File

@ -659,7 +659,10 @@ class Repositories(QObject):
.strip()
)
if not qgisMaximumVersion:
qgisMaximumVersion = qgisMinimumVersion[0] + ".99"
if qgisMinimumVersion[0] == "3" and supports_qt6:
qgisMaximumVersion = "4.99"
else:
qgisMaximumVersion = qgisMinimumVersion[0] + ".99"
# if compatible, add the plugin to the list
if not pluginNodes.item(i).firstChildElement(
"disabled"
@ -845,7 +848,10 @@ class Plugins(QObject):
qgisMinimumVersion = "0"
qgisMaximumVersion = pluginMetadata("qgisMaximumVersion").strip()
if not qgisMaximumVersion:
qgisMaximumVersion = qgisMinimumVersion[0] + ".99"
if qgisMinimumVersion[0] == "3" and supports_qt6:
qgisMaximumVersion = "4.99"
else:
qgisMaximumVersion = qgisMinimumVersion[0] + ".99"
# if compatible, add the plugin to the list
if not isCompatible(
pyQgisVersion(), qgisMinimumVersion, qgisMaximumVersion

View File

@ -23,6 +23,7 @@ try:
QgsServerProjectUtils.wmsInfoFormatSia2045 = staticmethod(QgsServerProjectUtils.wmsInfoFormatSia2045)
QgsServerProjectUtils.wmsFeatureInfoAddWktGeometry = staticmethod(QgsServerProjectUtils.wmsFeatureInfoAddWktGeometry)
QgsServerProjectUtils.wmsFeatureInfoUseAttributeFormSettings = staticmethod(QgsServerProjectUtils.wmsFeatureInfoUseAttributeFormSettings)
QgsServerProjectUtils.wmsHTMLFeatureInfoUseOnlyMaptip = staticmethod(QgsServerProjectUtils.wmsHTMLFeatureInfoUseOnlyMaptip)
QgsServerProjectUtils.wmsFeatureInfoSegmentizeWktGeometry = staticmethod(QgsServerProjectUtils.wmsFeatureInfoSegmentizeWktGeometry)
QgsServerProjectUtils.wmsAddLegendGroupsLegendGraphic = staticmethod(QgsServerProjectUtils.wmsAddLegendGroupsLegendGraphic)
QgsServerProjectUtils.wmsSkipNameForGroup = staticmethod(QgsServerProjectUtils.wmsSkipNameForGroup)

View File

@ -252,6 +252,20 @@ the feature info response
feature info response
%End
static bool wmsHTMLFeatureInfoUseOnlyMaptip( const QgsProject &project );
%Docstring
Returns if only the maptip should be used for HTML feature info response
so that the HTML response to the feature info request only contains the
maptip. If no maptip is set, the HTML response is empty.
:param project: the QGIS project
:return: true if only the maptip should be used for the feature info
response only
.. versionadded:: 4.0
%End
static bool wmsFeatureInfoSegmentizeWktGeometry( const QgsProject &project );
%Docstring
Returns if the geometry has to be segmentize in GetFeatureInfo request.

View File

@ -727,19 +727,25 @@ bool QgsPluginRegistry::checkPythonPlugin( const QString &packageName )
bool QgsPluginRegistry::isPythonPluginCompatible( const QString &packageName ) const
{
#ifdef WITH_BINDINGS
#if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 )
bool supportsQgis4 = true;
const QString supportsQt6 = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "supportsQt6" ) ).trimmed();
if ( supportsQt6.compare( QLatin1String( "YES" ), Qt::CaseInsensitive ) != 0 && supportsQt6.compare( QLatin1String( "TRUE" ), Qt::CaseInsensitive ) != 0 )
{
#if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 )
if ( !getenv( "QGIS_DISABLE_SUPPORTS_QT6_CHECK" ) )
{
return false;
}
}
#endif
supportsQgis4 = false;
}
const QString minVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMinimumVersion" ) );
// try to read qgisMaximumVersion. Note checkQgisVersion can cope with "__error__" value.
const QString maxVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMaximumVersion" ) );
QString maxVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMaximumVersion" ) );
if ( maxVersion == QLatin1String( "__error__" ) && minVersion.startsWith( QLatin1String( "3." ) ) && supportsQgis4 )
{
maxVersion = QLatin1String( "4.99.0" );
}
return minVersion != QLatin1String( "__error__" ) && checkQgisVersion( minVersion, maxVersion );
#else
Q_UNUSED( packageName )

View File

@ -736,6 +736,9 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa
bool useAttributeFormSettings = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMSFeatureInfoUseAttributeFormSettings" ), QStringLiteral( "/" ) );
mUseAttributeFormSettingsCheckBox->setChecked( useAttributeFormSettings );
bool useOnlyMaptip = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMSHTMLFeatureInfoUseOnlyMaptip" ), QStringLiteral( "/" ) );
mHTMLFiOnlyMaptip->setChecked( useOnlyMaptip );
bool addWktGeometry = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMSAddWktGeometry" ), QStringLiteral( "/" ) );
mAddWktGeometryCheckBox->setChecked( addWktGeometry );
@ -1566,6 +1569,7 @@ void QgsProjectProperties::apply()
}
QgsProject::instance()->writeEntry( QStringLiteral( "WMSFeatureInfoUseAttributeFormSettings" ), QStringLiteral( "/" ), mUseAttributeFormSettingsCheckBox->isChecked() );
QgsProject::instance()->writeEntry( QStringLiteral( "WMSHTMLFeatureInfoUseOnlyMaptip" ), QStringLiteral( "/" ), mHTMLFiOnlyMaptip->isChecked() );
QgsProject::instance()->writeEntry( QStringLiteral( "WMSAddWktGeometry" ), QStringLiteral( "/" ), mAddWktGeometryCheckBox->isChecked() );
QgsProject::instance()->writeEntry( QStringLiteral( "WMSSegmentizeFeatureInfoGeometry" ), QStringLiteral( "/" ), mSegmentizeFeatureInfoGeometryCheckBox->isChecked() );
QgsProject::instance()->writeEntry( QStringLiteral( "WMSAddLayerGroupsLegendGraphic" ), QStringLiteral( "/" ), mAddLayerGroupsLegendGraphicCheckBox->isChecked() );

View File

@ -1532,6 +1532,7 @@ void QgsLineString::visitPointsByRegularDistance( const double distance, const s
double pZ = std::numeric_limits<double>::quiet_NaN();
double pM = std::numeric_limits<double>::quiet_NaN();
double nextPointDistance = distance;
const double eps = 4 * nextPointDistance * std::numeric_limits<double>::epsilon ();
for ( int i = 1; i < totalPoints; ++i )
{
double thisX = *x++;
@ -1540,7 +1541,7 @@ void QgsLineString::visitPointsByRegularDistance( const double distance, const s
double thisM = m ? *m++ : 0.0;
const double segmentLength = QgsGeometryUtilsBase::distance2D( thisX, thisY, prevX, prevY );
while ( nextPointDistance < distanceTraversed + segmentLength || qgsDoubleNear( nextPointDistance, distanceTraversed + segmentLength ) )
while ( nextPointDistance < distanceTraversed + segmentLength || qgsDoubleNear( nextPointDistance, distanceTraversed + segmentLength, eps ) )
{
// point falls on this segment - truncate to segment length if qgsDoubleNear test was actually > segment length
const double distanceToPoint = std::min( nextPointDistance - distanceTraversed, segmentLength );

View File

@ -116,21 +116,6 @@ QStringList makeKeyTokens_( const QString &scope, const QString &key )
// be sure to include the canonical root node
keyTokens.push_front( QStringLiteral( "properties" ) );
//check validy of keys since an invalid xml name will will be dropped upon saving the xml file. If not valid, we print a message to the console.
for ( int i = 0; i < keyTokens.size(); ++i )
{
const QString keyToken = keyTokens.at( i );
//invalid chars in XML are found at http://www.w3.org/TR/REC-xml/#NT-NameChar
//note : it seems \x10000-\xEFFFF is valid, but it when added to the regexp, a lot of unwanted characters remain
const thread_local QRegularExpression sInvalidRegexp = QRegularExpression( QStringLiteral( "([^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\-\\.0-9\\x{B7}\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}]|^[^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}])" ) );
if ( keyToken.contains( sInvalidRegexp ) )
{
const QString errorString = QObject::tr( "Entry token invalid : '%1'. The token will not be saved to file." ).arg( keyToken );
QgsMessageLog::logMessage( errorString, QString(), Qgis::MessageLevel::Critical );
}
}
return keyTokens;
}
@ -1322,20 +1307,20 @@ void dump_( const QgsProjectPropertyKey &topQgsPropertyKey )
* scope. "layers" is a list containing three string values.
*
* \code{.xml}
* <properties>
* <fsplugin>
* <foo type="int" >42</foo>
* <baz type="int" >1</baz>
* <layers type="QStringList" >
* <properties name="properties">
* <properties name="fsplugin">
* <properties name="foo" type="int" >42</properties>
* <properties name="baz" type="int" >1</properties>
* <properties name="layers" type="QStringList">
* <value>railroad</value>
* <value>airport</value>
* </layers>
* <xyqzzy type="int" >1</xyqzzy>
* <bar type="double" >123.456</bar>
* <feature_types type="QStringList" >
* </properties>
* <properties name="xyqzzy" type="int" >1</properties>
* <properties name="bar" type="double" >123.456</properties>
* <properties name="feature_types" type="QStringList">
* <value>type</value>
* </feature_types>
* </fsplugin>
* </properties>
* </properties>
* </properties>
* \endcode
*
@ -3992,10 +3977,25 @@ bool QgsProject::createEmbeddedLayer( const QString &layerId, const QString &pro
const QDomElement propertiesElem = sProjectDocument.documentElement().firstChildElement( QStringLiteral( "properties" ) );
if ( !propertiesElem.isNull() )
{
const QDomElement absElem = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) ).firstChildElement( QStringLiteral( "Absolute" ) );
if ( !absElem.isNull() )
QDomElement e = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) );
if ( e.isNull() )
{
useAbsolutePaths = absElem.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
e = propertiesElem.firstChildElement( QStringLiteral( "properties" ) );
while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Paths" ) )
e = e.nextSiblingElement( QStringLiteral( "properties" ) );
e = e.firstChildElement( QStringLiteral( "properties" ) );
while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Absolute" ) )
e = e.nextSiblingElement( QStringLiteral( "properties" ) );
}
else
{
e = e.firstChildElement( QStringLiteral( "Absolute" ) );
}
if ( !e.isNull() )
{
useAbsolutePaths = e.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
}
}

View File

@ -233,15 +233,15 @@ bool QgsProjectPropertyValue::readXml( const QDomNode &keyNode )
// keyElement is created by parent QgsProjectPropertyKey
bool QgsProjectPropertyValue::writeXml( QString const &nodeName,
QDomElement &keyElement,
QDomDocument &document )
QDomElement &keyElement,
QDomDocument &document )
{
QDomElement valueElement = document.createElement( nodeName );
QDomElement valueElement = document.createElement( QStringLiteral( "properties" ) );
// remember the type so that we can rebuild it when the project is read in
valueElement.setAttribute( QStringLiteral( "name" ), nodeName );
valueElement.setAttribute( QStringLiteral( "type" ), mValue.typeName() );
// we handle string lists differently from other types in that we
// create a sequence of repeated elements to cover all the string list
// members; each value will be in a <value></value> tag.
@ -362,33 +362,41 @@ bool QgsProjectPropertyKey::readXml( const QDomNode &keyNode )
while ( i < subkeys.count() )
{
const QDomNode subkey = subkeys.item( i );
QString name;
if ( subkey.nodeName() == QStringLiteral( "properties" ) &&
subkey.hasAttributes() && // if we have attributes
subkey.isElement() && // and we're an element
subkey.toElement().hasAttribute( QStringLiteral( "name" ) ) ) // and we have a "name" attribute
name = subkey.toElement().attribute( QStringLiteral( "name" ) );
else
name = subkey.nodeName();
// if the current node is an element that has a "type" attribute,
// then we know it's a leaf node; i.e., a subkey _value_, and not
// a subkey
if ( subkeys.item( i ).hasAttributes() && // if we have attributes
subkeys.item( i ).isElement() && // and we're an element
subkeys.item( i ).toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute
if ( subkey.hasAttributes() && // if we have attributes
subkey.isElement() && // and we're an element
subkey.toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute
{
// then we're a key value
delete mProperties.take( subkeys.item( i ).nodeName() );
mProperties.insert( subkeys.item( i ).nodeName(), new QgsProjectPropertyValue );
//
delete mProperties.take( name );
mProperties.insert( name, new QgsProjectPropertyValue );
QDomNode subkey = subkeys.item( i );
if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) )
if ( !mProperties[name]->readXml( subkey ) )
{
QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( subkeys.item( i ).nodeName() ) );
QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( name ) );
}
}
else // otherwise it's a subkey, so just recurse on down the remaining keys
{
addKey( subkeys.item( i ).nodeName() );
addKey( name );
QDomNode subkey = subkeys.item( i );
if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) )
if ( !mProperties[name]->readXml( subkey ) )
{
QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( subkeys.item( i ).nodeName() ) );
QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( name ) );
}
}
@ -408,7 +416,8 @@ bool QgsProjectPropertyKey::writeXml( QString const &nodeName, QDomElement &elem
// If it's an _empty_ node (i.e., one with no properties) we need to emit
// an empty place holder; else create new Dom elements as necessary.
QDomElement keyElement = document.createElement( nodeName ); // Dom element for this property key
QDomElement keyElement = document.createElement( "properties" ); // Dom element for this property key
keyElement.toElement().setAttribute( QStringLiteral( "name" ), nodeName );
if ( ! mProperties.isEmpty() )
{

View File

@ -37,6 +37,8 @@ QString QgsProjectServerValidator::displayValidationError( QgsProjectServerValid
return QObject::tr( "The project root name (either the project short name or project title) is not valid. It must start with an unaccented alphabetical letter, followed by any alphanumeric letters, dot, dash or underscore" );
case QgsProjectServerValidator::ProjectRootNameConflict:
return QObject::tr( "The project root name (either the project short name or project title) is already used by a layer or a group" );
case QgsProjectServerValidator::OnlyMaptipTrueButEmptyMaptip:
return QObject::tr( "Use only maptip for HTML GetFeatureInfo response is enabled but the HTML maptip is empty" );
}
return QString();
}
@ -49,7 +51,7 @@ QString getShortName( T *node )
return shortName.isEmpty() ? node->name() : shortName;
}
void QgsProjectServerValidator::browseLayerTree( QgsLayerTreeGroup *treeGroup, QStringList &owsNames, QStringList &encodingMessages )
void QgsProjectServerValidator::browseLayerTree( QgsLayerTreeGroup *treeGroup, QStringList &owsNames, QStringList &encodingMessages, QStringList &layerNames, QStringList &maptipTemplates )
{
const QList< QgsLayerTreeNode * > treeGroupChildren = treeGroup->children();
for ( int i = 0; i < treeGroupChildren.size(); ++i )
@ -59,7 +61,7 @@ void QgsProjectServerValidator::browseLayerTree( QgsLayerTreeGroup *treeGroup, Q
{
QgsLayerTreeGroup *treeGroupChild = static_cast<QgsLayerTreeGroup *>( treeNode );
owsNames << getShortName( treeGroupChild );
browseLayerTree( treeGroupChild, owsNames, encodingMessages );
browseLayerTree( treeGroupChild, owsNames, encodingMessages, layerNames, maptipTemplates );
}
else
{
@ -74,11 +76,22 @@ void QgsProjectServerValidator::browseLayerTree( QgsLayerTreeGroup *treeGroup, Q
if ( vl->dataProvider() && vl->dataProvider()->encoding() == QLatin1String( "System" ) )
encodingMessages << layer->name();
}
layerNames << treeLayer->name();
maptipTemplates << layer->mapTipTemplate();
}
}
}
}
bool QgsProjectServerValidator::isOnlyMaptipEnabled( QgsProject *project )
{
return project->readBoolEntry(
QStringLiteral( "WMSHTMLFeatureInfoUseOnlyMaptip" ),
QString(),
false
);
}
bool QgsProjectServerValidator::validate( QgsProject *project, QList<QgsProjectServerValidator::ValidationResult> &results )
{
results.clear();
@ -90,8 +103,8 @@ bool QgsProjectServerValidator::validate( QgsProject *project, QList<QgsProjectS
if ( !project->layerTreeRoot() )
return false;
QStringList owsNames, encodingMessages;
browseLayerTree( project->layerTreeRoot(), owsNames, encodingMessages );
QStringList owsNames, encodingMessages, layerNames, maptipTemplates;
browseLayerTree( project->layerTreeRoot(), owsNames, encodingMessages, layerNames, maptipTemplates );
QStringList duplicateNames, regExpMessages;
const thread_local QRegularExpression snRegExp = QgsApplication::shortNameRegularExpression();
@ -152,6 +165,24 @@ bool QgsProjectServerValidator::validate( QgsProject *project, QList<QgsProjectS
results << ValidationResult( QgsProjectServerValidator::ProjectShortName, rootLayerName );
}
}
if ( isOnlyMaptipEnabled( project ) )
{
QStringList emptyLayers;
for ( int i = 0; i < maptipTemplates.size(); ++i )
{
if ( maptipTemplates[i].trimmed().isEmpty() )
emptyLayers << layerNames[i];
}
if ( !emptyLayers.isEmpty() )
{
result = false;
QString details = emptyLayers.join( QLatin1String( ", " ) ).toHtmlEscaped();
results << ValidationResult(
QgsProjectServerValidator::OnlyMaptipTrueButEmptyMaptip,
details );
}
}
return result;
}

View File

@ -47,6 +47,7 @@ class CORE_EXPORT QgsProjectServerValidator
LayerEncoding = 2, //!< Encoding is not correctly set on a vector layer.
ProjectShortName = 3, //!< The project short name is not valid.
ProjectRootNameConflict = 4, //!< The project root name is already used by a layer or a group.
OnlyMaptipTrueButEmptyMaptip = 5, //!< Use only maptip for HTML GetFeatureInfo response is enabled but HTML maptip is empty
};
/**
@ -92,7 +93,8 @@ class CORE_EXPORT QgsProjectServerValidator
static bool validate( QgsProject *project, QList< QgsProjectServerValidator::ValidationResult > &results SIP_OUT );
private:
static void browseLayerTree( QgsLayerTreeGroup *treeGroup, QStringList &owsNames, QStringList &encodingMessages );
static void browseLayerTree( QgsLayerTreeGroup *treeGroup, QStringList &owsNames, QStringList &encodingMessages, QStringList &layerNames, QStringList &maptipTemplates );
static bool isOnlyMaptipEnabled( QgsProject *project );
};

View File

@ -180,6 +180,12 @@ bool QgsServerProjectUtils::wmsFeatureInfoUseAttributeFormSettings( const QgsPro
|| useFormSettings.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
}
bool QgsServerProjectUtils::wmsHTMLFeatureInfoUseOnlyMaptip( const QgsProject &project )
{
const QString useFormSettings = project.readEntry( QStringLiteral( "WMSHTMLFeatureInfoUseOnlyMaptip" ), QStringLiteral( "/" ), "" );
return useFormSettings.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
}
bool QgsServerProjectUtils::wmsFeatureInfoSegmentizeWktGeometry( const QgsProject &project )
{
const QString segmGeom = project.readEntry( QStringLiteral( "WMSSegmentizeFeatureInfoGeometry" ), QStringLiteral( "/" ), "" );

View File

@ -222,6 +222,16 @@ class SERVER_EXPORT QgsServerProjectUtils
*/
static bool wmsFeatureInfoUseAttributeFormSettings( const QgsProject &project );
/**
* Returns if only the maptip should be used for HTML feature info response so
* that the HTML response to the feature info request only contains the maptip.
* If no maptip is set, the HTML response is empty.
* \param project the QGIS project
* \returns true if only the maptip should be used for the feature info response only
* \since QGIS 4.0
*/
static bool wmsHTMLFeatureInfoUseOnlyMaptip( const QgsProject &project );
/**
* Returns if the geometry has to be segmentize in GetFeatureInfo request.
* \param project the QGIS project

View File

@ -2009,7 +2009,7 @@ namespace QgsWms
//add maptip attribute based on html/expression (in case there is no maptip attribute)
QString mapTip = layer->mapTipTemplate();
if ( !mapTip.isEmpty() && ( mWmsParameters.withMapTip() || mWmsParameters.htmlInfoOnlyMapTip() ) )
if ( !mapTip.isEmpty() && ( mWmsParameters.withMapTip() || mWmsParameters.htmlInfoOnlyMapTip() || QgsServerProjectUtils::wmsHTMLFeatureInfoUseOnlyMaptip( *mProject ) ) )
{
QDomElement maptipElem = infoDocument.createElement( QStringLiteral( "Attribute" ) );
maptipElem.setAttribute( QStringLiteral( "name" ), QStringLiteral( "maptip" ) );
@ -2325,7 +2325,7 @@ namespace QgsWms
}
//add maptip attribute based on html/expression
QString mapTip = layer->mapTipTemplate();
if ( !mapTip.isEmpty() && ( mWmsParameters.withMapTip() || mWmsParameters.htmlInfoOnlyMapTip() ) )
if ( !mapTip.isEmpty() && ( mWmsParameters.withMapTip() || mWmsParameters.htmlInfoOnlyMapTip() || QgsServerProjectUtils::wmsHTMLFeatureInfoUseOnlyMaptip( *mProject ) ) )
{
QDomElement maptipElem = infoDocument.createElement( QStringLiteral( "Attribute" ) );
maptipElem.setAttribute( QStringLiteral( "name" ), QStringLiteral( "maptip" ) );
@ -2586,7 +2586,7 @@ namespace QgsWms
QByteArray QgsRenderer::convertFeatureInfoToHtml( const QDomDocument &doc ) const
{
const bool onlyMapTip = mWmsParameters.htmlInfoOnlyMapTip();
const bool onlyMapTip = mWmsParameters.htmlInfoOnlyMapTip() || QgsServerProjectUtils::wmsHTMLFeatureInfoUseOnlyMaptip( *mProject );
QString featureInfoString = QStringLiteral( " <!DOCTYPE html>" );
if ( !onlyMapTip )
{
@ -3203,7 +3203,7 @@ namespace QgsWms
{
QString mapTip = layer->mapTipTemplate();
if ( !mapTip.isEmpty() && ( mWmsParameters.withMapTip() || mWmsParameters.htmlInfoOnlyMapTip() ) )
if ( !mapTip.isEmpty() && ( mWmsParameters.withMapTip() || mWmsParameters.htmlInfoOnlyMapTip() || QgsServerProjectUtils::wmsHTMLFeatureInfoUseOnlyMaptip( *mProject ) ) )
{
QString fieldTextString = QgsExpression::replaceExpressionText( mapTip, &expressionContext );
QDomElement fieldElem = doc.createElement( QStringLiteral( "qgs:maptip" ) );

View File

@ -2730,6 +2730,13 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="mHTMLFiOnlyMaptip">
<property name="text">
<string>Use only maptip for HTML GetFeatureInfo (empty response when maptip template is missing)</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="mWmsUseLayerIDs">
<property name="text">
@ -2737,7 +2744,7 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<layout class="QHBoxLayout" name="grpWMSPrecision">
<item>
<widget class="QLabel" name="label_5">
@ -2761,21 +2768,21 @@
</item>
</layout>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QCheckBox" name="mSegmentizeFeatureInfoGeometryCheckBox">
<property name="text">
<string>Segmentize feature info geometry</string>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QCheckBox" name="mAddWktGeometryCheckBox">
<property name="text">
<string>Add geometry to feature response</string>
</property>
</widget>
</item>
<item row="5" column="0">
<item row="6" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="mWMSUrlLabel">
@ -3765,6 +3772,7 @@
<tabstop>mWMSInspireMetadataDate</tabstop>
<tabstop>mWmsUseLayerIDs</tabstop>
<tabstop>mUseAttributeFormSettingsCheckBox</tabstop>
<tabstop>mHTMLFiOnlyMaptip</tabstop>
<tabstop>mAddWktGeometryCheckBox</tabstop>
<tabstop>mSegmentizeFeatureInfoGeometryCheckBox</tabstop>
<tabstop>mWMSPrecisionSpinBox</tabstop>

View File

@ -65,84 +65,6 @@ class TestQgsProject(QgisTestCase):
QgisTestCase.__init__(self, methodName)
self.messageCaught = False
def test_makeKeyTokens_(self):
# see http://www.w3.org/TR/REC-xml/#d0e804 for a list of valid characters
invalidTokens = []
validTokens = []
# all test tokens will be generated by prepending or inserting characters to this token
validBase = "valid"
# some invalid characters, not allowed anywhere in a token
# note that '/' must not be added here because it is taken as a separator by makeKeyTokens_()
invalidChars = "+*,;<>|!$%()=?#\x01"
# generate the characters that are allowed at the start of a token (and at every other position)
validStartChars = ":_"
charRanges = [
(ord("a"), ord("z")),
(ord("A"), ord("Z")),
(0x00F8, 0x02FF),
(0x0370, 0x037D),
(0x037F, 0x1FFF),
(0x200C, 0x200D),
(0x2070, 0x218F),
(0x2C00, 0x2FEF),
(0x3001, 0xD7FF),
(0xF900, 0xFDCF),
(0xFDF0, 0xFFFD),
# (0x10000, 0xEFFFF), while actually valid, these are not yet accepted by makeKeyTokens_()
]
for r in charRanges:
for c in range(r[0], r[1]):
validStartChars += chr(c)
# generate the characters that are only allowed inside a token, not at the start
validInlineChars = "-.\xB7"
charRanges = [
(ord("0"), ord("9")),
(0x0300, 0x036F),
(0x203F, 0x2040),
]
for r in charRanges:
for c in range(r[0], r[1]):
validInlineChars += chr(c)
# test forbidden start characters
for c in invalidChars + validInlineChars:
invalidTokens.append(c + validBase)
# test forbidden inline characters
for c in invalidChars:
invalidTokens.append(validBase[:4] + c + validBase[4:])
# test each allowed start character
for c in validStartChars:
validTokens.append(c + validBase)
# test each allowed inline character
for c in validInlineChars:
validTokens.append(validBase[:4] + c + validBase[4:])
logger = QgsApplication.messageLog()
logger.messageReceived.connect(self.catchMessage)
prj = QgsProject.instance()
for token in validTokens:
self.messageCaught = False
prj.readEntry("test", token)
myMessage = f"valid token '{token}' not accepted"
assert not self.messageCaught, myMessage
for token in invalidTokens:
self.messageCaught = False
prj.readEntry("test", token)
myMessage = f"invalid token '{token}' accepted"
assert self.messageCaught, myMessage
logger.messageReceived.disconnect(self.catchMessage)
def catchMessage(self):
self.messageCaught = True

View File

@ -124,6 +124,60 @@ class TestQgsprojectServerValidator(QgisTestCase):
results[0].error,
)
def test_empty_maptip_enabled(self):
"""Empty maptip must fail when onlymaptip is enabled."""
project = QgsProject()
layer = QgsVectorLayer("Point?field=fldtxt:string", "testlayer", "memory")
project.addMapLayers([layer])
layer.setMapTipTemplate("")
project.writeEntry("WMSHTMLFeatureInfoUseOnlyMaptip", "", True)
valid, results = QgsProjectServerValidator.validate(project)
self.assertFalse(valid)
self.assertEqual(1, len(results))
self.assertEqual(
QgsProjectServerValidator.ValidationError.OnlyMaptipTrueButEmptyMaptip,
results[0].error,
)
# Explicitly enable MapTips — should still fail
layer.setMapTipsEnabled(True)
valid2, results2 = QgsProjectServerValidator.validate(project)
self.assertFalse(valid2)
self.assertEqual(1, len(results2))
self.assertEqual(
QgsProjectServerValidator.ValidationError.OnlyMaptipTrueButEmptyMaptip,
results2[0].error,
)
# Explicitly disable MapTips — should still fail
layer.setMapTipsEnabled(False)
valid3, results3 = QgsProjectServerValidator.validate(project)
self.assertFalse(valid3)
self.assertEqual(1, len(results3))
self.assertEqual(
QgsProjectServerValidator.ValidationError.OnlyMaptipTrueButEmptyMaptip,
results3[0].error,
)
def test_empty_maptip_disabled(self):
"""Empty maptip must pass when onlymaptip is disabled."""
project = QgsProject()
layer = QgsVectorLayer("Point?field=fldtxt:string", "testlayer", "memory")
project.addMapLayers([layer])
layer.setMapTipTemplate("")
project.writeEntry("WMSHTMLFeatureInfoUseOnlyMaptip", "", False)
valid, results = QgsProjectServerValidator.validate(project)
self.assertTrue(valid)
self.assertEqual(0, len(results))
# Explicitly enable MapTips — should still pass
layer.setMapTipsEnabled(True)
valid2, results2 = QgsProjectServerValidator.validate(project)
self.assertTrue(valid2)
self.assertEqual(0, len(results2))
# Explicitly disable MapTips — should still pass
layer.setMapTipsEnabled(False)
valid3, results3 = QgsProjectServerValidator.validate(project)
self.assertTrue(valid3)
self.assertEqual(0, len(results3))
if __name__ == "__main__":
unittest.main()

View File

@ -174,7 +174,7 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
"wms_getfeatureinfo-text-html-maptip",
)
# Test getfeatureinfo response html only with maptip for vector layer
# Test getfeatureinfo response html only with maptip for vector layer (URL parameter)
self.wms_request_compare(
"GetFeatureInfo",
"&layers=testlayer%20%C3%A8%C3%A9&styles=&"
@ -186,6 +186,18 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
"wms_getfeatureinfo-html-only-with-maptip-vector",
)
# Test getfeatureinfo response html only with maptip for vector layer (project settings)
self.wms_request_compare(
"GetFeatureInfo",
"&layers=testlayer%20%C3%A8%C3%A9&styles=&"
+ "info_format=text%2Fhtml&transparent=true&"
+ "width=600&height=400&srs=EPSG%3A3857&bbox=913190.6389747962%2C"
+ "5606005.488876367%2C913235.426296057%2C5606035.347090538&"
+ "query_layers=testlayer%20%C3%A8%C3%A9&X=190&Y=320",
"wms_getfeatureinfo-html-only-with-maptip-vector",
"test_project_html_gfi_maptip_only.qgs",
)
# Test getfeatureinfo response html with maptip and display name in text mode for vector layer
self.wms_request_compare(
"GetFeatureInfo",
@ -352,7 +364,7 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
"wms_getfeatureinfo-raster-text-xml-maptip",
)
# Test GetFeatureInfo on raster layer HTML only with maptip
# Test GetFeatureInfo on raster layer HTML only with maptip (URL parameter)
self.wms_request_compare(
"GetFeatureInfo",
"&layers=landsat&styles=&"
@ -364,6 +376,18 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
"wms_getfeatureinfo-html-only-with-maptip-raster",
)
# Test GetFeatureInfo on raster layer HTML only with maptip (project settings)
self.wms_request_compare(
"GetFeatureInfo",
"&layers=landsat&styles=&"
+ "info_format=text%2Fhtml&transparent=true&"
+ "width=500&height=500&srs=EPSG%3A3857&"
+ "bbox=1989139.6,3522745.0,2015014.9,3537004.5&"
+ "query_layers=landsat&X=250&Y=250",
"wms_getfeatureinfo-html-only-with-maptip-raster",
"test_project_html_gfi_maptip_only.qgs",
)
def testGetFeatureInfoValueRelation(self):
"""Test GetFeatureInfo resolves "value relation" widget values. regression 18518"""
mypath = self.testdata_path + "test_project_values.qgz"

File diff suppressed because it is too large Load Diff