|
| 1 | +commit 74549aad26c3358101e88477d9dfa1caae013d72 |
| 2 | +Author: Jürgen E. Fischer <jef@norbit.de> |
| 3 | +Date: Fri Jun 20 15:58:30 2025 +0200 |
| 4 | + |
| 5 | + Reapply "Allow free naming of project properties (#60855)" |
| 6 | + |
| 7 | + This reverts commit fb11239112adfc321b3bbacbb20da888a7a37c23. |
| 8 | + |
| 9 | +diff --git a/src/core/project/qgsproject.cpp b/src/core/project/qgsproject.cpp |
| 10 | +index f78f9e53bef..cd6f78edaaf 100644 |
| 11 | +--- a/src/core/project/qgsproject.cpp |
| 12 | ++++ b/src/core/project/qgsproject.cpp |
| 13 | +@@ -116,21 +116,6 @@ QStringList makeKeyTokens_( const QString &scope, const QString &key ) |
| 14 | + // be sure to include the canonical root node |
| 15 | + keyTokens.push_front( QStringLiteral( "properties" ) ); |
| 16 | + |
| 17 | +- //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. |
| 18 | +- for ( int i = 0; i < keyTokens.size(); ++i ) |
| 19 | +- { |
| 20 | +- const QString keyToken = keyTokens.at( i ); |
| 21 | +- |
| 22 | +- //invalid chars in XML are found at http://www.w3.org/TR/REC-xml/#NT-NameChar |
| 23 | +- //note : it seems \x10000-\xEFFFF is valid, but it when added to the regexp, a lot of unwanted characters remain |
| 24 | +- 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}])" ) ); |
| 25 | +- if ( keyToken.contains( sInvalidRegexp ) ) |
| 26 | +- { |
| 27 | +- const QString errorString = QObject::tr( "Entry token invalid : '%1'. The token will not be saved to file." ).arg( keyToken ); |
| 28 | +- QgsMessageLog::logMessage( errorString, QString(), Qgis::MessageLevel::Critical ); |
| 29 | +- } |
| 30 | +- } |
| 31 | +- |
| 32 | + return keyTokens; |
| 33 | + } |
| 34 | + |
| 35 | +@@ -1322,20 +1307,20 @@ void dump_( const QgsProjectPropertyKey &topQgsPropertyKey ) |
| 36 | + * scope. "layers" is a list containing three string values. |
| 37 | + * |
| 38 | + * \code{.xml} |
| 39 | +- * <properties> |
| 40 | +- * <fsplugin> |
| 41 | +- * <foo type="int" >42</foo> |
| 42 | +- * <baz type="int" >1</baz> |
| 43 | +- * <layers type="QStringList" > |
| 44 | ++ * <properties name="properties"> |
| 45 | ++ * <properties name="fsplugin"> |
| 46 | ++ * <properties name="foo" type="int" >42</properties> |
| 47 | ++ * <properties name="baz" type="int" >1</properties> |
| 48 | ++ * <properties name="layers" type="QStringList"> |
| 49 | + * <value>railroad</value> |
| 50 | + * <value>airport</value> |
| 51 | +- * </layers> |
| 52 | +- * <xyqzzy type="int" >1</xyqzzy> |
| 53 | +- * <bar type="double" >123.456</bar> |
| 54 | +- * <feature_types type="QStringList" > |
| 55 | ++ * </properties> |
| 56 | ++ * <properties name="xyqzzy" type="int" >1</properties> |
| 57 | ++ * <properties name="bar" type="double" >123.456</properties> |
| 58 | ++ * <properties name="feature_types" type="QStringList"> |
| 59 | + * <value>type</value> |
| 60 | +- * </feature_types> |
| 61 | +- * </fsplugin> |
| 62 | ++ * </properties> |
| 63 | ++ * </properties> |
| 64 | + * </properties> |
| 65 | + * \endcode |
| 66 | + * |
| 67 | +@@ -3992,10 +3977,25 @@ bool QgsProject::createEmbeddedLayer( const QString &layerId, const QString &pro |
| 68 | + const QDomElement propertiesElem = sProjectDocument.documentElement().firstChildElement( QStringLiteral( "properties" ) ); |
| 69 | + if ( !propertiesElem.isNull() ) |
| 70 | + { |
| 71 | +- const QDomElement absElem = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) ).firstChildElement( QStringLiteral( "Absolute" ) ); |
| 72 | +- if ( !absElem.isNull() ) |
| 73 | ++ QDomElement e = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) ); |
| 74 | ++ if ( e.isNull() ) |
| 75 | ++ { |
| 76 | ++ e = propertiesElem.firstChildElement( QStringLiteral( "properties" ) ); |
| 77 | ++ while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Paths" ) ) |
| 78 | ++ e = e.nextSiblingElement( QStringLiteral( "properties" ) ); |
| 79 | ++ |
| 80 | ++ e = e.firstChildElement( QStringLiteral( "properties" ) ); |
| 81 | ++ while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Absolute" ) ) |
| 82 | ++ e = e.nextSiblingElement( QStringLiteral( "properties" ) ); |
| 83 | ++ } |
| 84 | ++ else |
| 85 | ++ { |
| 86 | ++ e = e.firstChildElement( QStringLiteral( "Absolute" ) ); |
| 87 | ++ } |
| 88 | ++ |
| 89 | ++ if ( !e.isNull() ) |
| 90 | + { |
| 91 | +- useAbsolutePaths = absElem.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0; |
| 92 | ++ useAbsolutePaths = e.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | +diff --git a/src/core/project/qgsprojectproperty.cpp b/src/core/project/qgsprojectproperty.cpp |
| 97 | +index ff8024a5260..1af598012b4 100644 |
| 98 | +--- a/src/core/project/qgsprojectproperty.cpp |
| 99 | ++++ b/src/core/project/qgsprojectproperty.cpp |
| 100 | +@@ -233,15 +233,15 @@ bool QgsProjectPropertyValue::readXml( const QDomNode &keyNode ) |
| 101 | + |
| 102 | + // keyElement is created by parent QgsProjectPropertyKey |
| 103 | + bool QgsProjectPropertyValue::writeXml( QString const &nodeName, |
| 104 | +- QDomElement &keyElement, |
| 105 | +- QDomDocument &document ) |
| 106 | ++ QDomElement &keyElement, |
| 107 | ++ QDomDocument &document ) |
| 108 | + { |
| 109 | +- QDomElement valueElement = document.createElement( nodeName ); |
| 110 | ++ QDomElement valueElement = document.createElement( QStringLiteral( "properties" ) ); |
| 111 | + |
| 112 | + // remember the type so that we can rebuild it when the project is read in |
| 113 | ++ valueElement.setAttribute( QStringLiteral( "name" ), nodeName ); |
| 114 | + valueElement.setAttribute( QStringLiteral( "type" ), mValue.typeName() ); |
| 115 | + |
| 116 | +- |
| 117 | + // we handle string lists differently from other types in that we |
| 118 | + // create a sequence of repeated elements to cover all the string list |
| 119 | + // members; each value will be in a <value></value> tag. |
| 120 | +@@ -362,33 +362,41 @@ bool QgsProjectPropertyKey::readXml( const QDomNode &keyNode ) |
| 121 | + |
| 122 | + while ( i < subkeys.count() ) |
| 123 | + { |
| 124 | ++ const QDomNode subkey = subkeys.item( i ); |
| 125 | ++ QString name; |
| 126 | ++ |
| 127 | ++ if ( subkey.nodeName() == QStringLiteral( "properties" ) && |
| 128 | ++ subkey.hasAttributes() && // if we have attributes |
| 129 | ++ subkey.isElement() && // and we're an element |
| 130 | ++ subkey.toElement().hasAttribute( QStringLiteral( "name" ) ) ) // and we have a "name" attribute |
| 131 | ++ name = subkey.toElement().attribute( QStringLiteral( "name" ) ); |
| 132 | ++ else |
| 133 | ++ name = subkey.nodeName(); |
| 134 | ++ |
| 135 | + // if the current node is an element that has a "type" attribute, |
| 136 | + // then we know it's a leaf node; i.e., a subkey _value_, and not |
| 137 | + // a subkey |
| 138 | +- if ( subkeys.item( i ).hasAttributes() && // if we have attributes |
| 139 | +- subkeys.item( i ).isElement() && // and we're an element |
| 140 | +- subkeys.item( i ).toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute |
| 141 | ++ if ( subkey.hasAttributes() && // if we have attributes |
| 142 | ++ subkey.isElement() && // and we're an element |
| 143 | ++ subkey.toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute |
| 144 | + { |
| 145 | + // then we're a key value |
| 146 | +- delete mProperties.take( subkeys.item( i ).nodeName() ); |
| 147 | +- mProperties.insert( subkeys.item( i ).nodeName(), new QgsProjectPropertyValue ); |
| 148 | ++ // |
| 149 | ++ delete mProperties.take( name ); |
| 150 | ++ mProperties.insert( name, new QgsProjectPropertyValue ); |
| 151 | + |
| 152 | +- QDomNode subkey = subkeys.item( i ); |
| 153 | +- |
| 154 | +- if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) ) |
| 155 | ++ if ( !mProperties[name]->readXml( subkey ) ) |
| 156 | + { |
| 157 | +- QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( subkeys.item( i ).nodeName() ) ); |
| 158 | ++ QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( name ) ); |
| 159 | + } |
| 160 | + } |
| 161 | + else // otherwise it's a subkey, so just recurse on down the remaining keys |
| 162 | + { |
| 163 | +- addKey( subkeys.item( i ).nodeName() ); |
| 164 | +- |
| 165 | +- QDomNode subkey = subkeys.item( i ); |
| 166 | ++ addKey( name ); |
| 167 | + |
| 168 | +- if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) ) |
| 169 | ++ if ( !mProperties[name]->readXml( subkey ) ) |
| 170 | + { |
| 171 | +- QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( subkeys.item( i ).nodeName() ) ); |
| 172 | ++ QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( name ) ); |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | +@@ -408,7 +416,8 @@ bool QgsProjectPropertyKey::writeXml( QString const &nodeName, QDomElement &elem |
| 177 | + // If it's an _empty_ node (i.e., one with no properties) we need to emit |
| 178 | + // an empty place holder; else create new Dom elements as necessary. |
| 179 | + |
| 180 | +- QDomElement keyElement = document.createElement( nodeName ); // Dom element for this property key |
| 181 | ++ QDomElement keyElement = document.createElement( "properties" ); // Dom element for this property key |
| 182 | ++ keyElement.toElement().setAttribute( QStringLiteral( "name" ), nodeName ); |
| 183 | + |
| 184 | + if ( ! mProperties.isEmpty() ) |
| 185 | + { |
| 186 | +diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py |
| 187 | +index 237553260f6..d44d4438006 100644 |
| 188 | +--- a/tests/src/python/test_qgsproject.py |
| 189 | ++++ b/tests/src/python/test_qgsproject.py |
| 190 | +@@ -65,84 +65,6 @@ class TestQgsProject(QgisTestCase): |
| 191 | + QgisTestCase.__init__(self, methodName) |
| 192 | + self.messageCaught = False |
| 193 | + |
| 194 | +- def test_makeKeyTokens_(self): |
| 195 | +- # see http://www.w3.org/TR/REC-xml/#d0e804 for a list of valid characters |
| 196 | +- |
| 197 | +- invalidTokens = [] |
| 198 | +- validTokens = [] |
| 199 | +- |
| 200 | +- # all test tokens will be generated by prepending or inserting characters to this token |
| 201 | +- validBase = "valid" |
| 202 | +- |
| 203 | +- # some invalid characters, not allowed anywhere in a token |
| 204 | +- # note that '/' must not be added here because it is taken as a separator by makeKeyTokens_() |
| 205 | +- invalidChars = "+*,;<>|!$%()=?#\x01" |
| 206 | +- |
| 207 | +- # generate the characters that are allowed at the start of a token (and at every other position) |
| 208 | +- validStartChars = ":_" |
| 209 | +- charRanges = [ |
| 210 | +- (ord("a"), ord("z")), |
| 211 | +- (ord("A"), ord("Z")), |
| 212 | +- (0x00F8, 0x02FF), |
| 213 | +- (0x0370, 0x037D), |
| 214 | +- (0x037F, 0x1FFF), |
| 215 | +- (0x200C, 0x200D), |
| 216 | +- (0x2070, 0x218F), |
| 217 | +- (0x2C00, 0x2FEF), |
| 218 | +- (0x3001, 0xD7FF), |
| 219 | +- (0xF900, 0xFDCF), |
| 220 | +- (0xFDF0, 0xFFFD), |
| 221 | +- # (0x10000, 0xEFFFF), while actually valid, these are not yet accepted by makeKeyTokens_() |
| 222 | +- ] |
| 223 | +- for r in charRanges: |
| 224 | +- for c in range(r[0], r[1]): |
| 225 | +- validStartChars += chr(c) |
| 226 | +- |
| 227 | +- # generate the characters that are only allowed inside a token, not at the start |
| 228 | +- validInlineChars = "-.\xB7" |
| 229 | +- charRanges = [ |
| 230 | +- (ord("0"), ord("9")), |
| 231 | +- (0x0300, 0x036F), |
| 232 | +- (0x203F, 0x2040), |
| 233 | +- ] |
| 234 | +- for r in charRanges: |
| 235 | +- for c in range(r[0], r[1]): |
| 236 | +- validInlineChars += chr(c) |
| 237 | +- |
| 238 | +- # test forbidden start characters |
| 239 | +- for c in invalidChars + validInlineChars: |
| 240 | +- invalidTokens.append(c + validBase) |
| 241 | +- |
| 242 | +- # test forbidden inline characters |
| 243 | +- for c in invalidChars: |
| 244 | +- invalidTokens.append(validBase[:4] + c + validBase[4:]) |
| 245 | +- |
| 246 | +- # test each allowed start character |
| 247 | +- for c in validStartChars: |
| 248 | +- validTokens.append(c + validBase) |
| 249 | +- |
| 250 | +- # test each allowed inline character |
| 251 | +- for c in validInlineChars: |
| 252 | +- validTokens.append(validBase[:4] + c + validBase[4:]) |
| 253 | +- |
| 254 | +- logger = QgsApplication.messageLog() |
| 255 | +- logger.messageReceived.connect(self.catchMessage) |
| 256 | +- prj = QgsProject.instance() |
| 257 | +- |
| 258 | +- for token in validTokens: |
| 259 | +- self.messageCaught = False |
| 260 | +- prj.readEntry("test", token) |
| 261 | +- myMessage = f"valid token '{token}' not accepted" |
| 262 | +- assert not self.messageCaught, myMessage |
| 263 | +- |
| 264 | +- for token in invalidTokens: |
| 265 | +- self.messageCaught = False |
| 266 | +- prj.readEntry("test", token) |
| 267 | +- myMessage = f"invalid token '{token}' accepted" |
| 268 | +- assert self.messageCaught, myMessage |
| 269 | +- |
| 270 | +- logger.messageReceived.disconnect(self.catchMessage) |
| 271 | +- |
| 272 | + def catchMessage(self): |
| 273 | + self.messageCaught = True |
| 274 | + |
0 commit comments