diff --git a/python/core/qgsproject.sip b/python/core/qgsproject.sip index 6a24cb39309..6df43302744 100644 --- a/python/core/qgsproject.sip +++ b/python/core/qgsproject.sip @@ -150,7 +150,7 @@ class QgsProject : QObject keys would be the familiar QSettings-like '/' delimited entries, implying a hierarchy of keys and corresponding values - @note The key string must include '/'s. E.g., "/foo" not "foo". + @note The key string must be valid xml tag names in order to be saved to the file. */ //@{ //! @note available in python bindings as writeEntryBool @@ -167,8 +167,6 @@ class QgsProject : QObject keys would be the familiar QSettings-like '/' delimited entries, implying a hierarchy of keys and corresponding values - - @note The key string must include '/'s. E.g., "/foo" not "foo". */ //@{ QStringList readListEntry( const QString & scope, const QString & key, const QStringList& def = QStringList(), bool *ok = 0 ) const; diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp index bb5c770230c..d6156e6068f 100644 --- a/src/core/qgsproject.cpp +++ b/src/core/qgsproject.cpp @@ -27,6 +27,7 @@ #include "qgslayertreeregistrybridge.h" #include "qgslogger.h" #include "qgsmaplayerregistry.h" +#include "qgsmessagelog.h" #include "qgspluginlayer.h" #include "qgspluginlayerregistry.h" #include "qgsprojectfiletransform.h" @@ -66,6 +67,24 @@ QStringList makeKeyTokens_( QString const &scope, QString const &key ) // be sure to include the canonical root node keyTokens.push_front( "properties" ); + //check validy of keys since an unvalid 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){ + 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 + QString nameCharRegexp = QString( "[^:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x2FF\\x370-\\x37D\\x37F-\\x1FFF\\x200C-\\x200D\\x2070-\\x218F\\x2C00-\\x2FEF\\x3001-\\xD7FF\\xF900-\\xFDCF\\xFDF0-\\xFFFD\\-\\.0-9\\xB7\\x0300-\\x036F\\x203F-\\x2040]" ); + QString nameStartCharRegexp = QString( "^[^:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x2FF\\x370-\\x37D\\x37F-\\x1FFF\\x200C-\\x200D\\x2070-\\x218F\\x2C00-\\x2FEF\\x3001-\\xD7FF\\xF900-\\xFDCF\\xFDF0-\\xFFFD]" ); + + if( keyToken.contains( QRegExp(nameCharRegexp) ) || keyToken.contains( QRegExp(nameStartCharRegexp) ) ){ + + QString errorString = QObject::tr("Entry token invalid : '%1'. The token will not be saved to file.").arg(keyToken); + QgsMessageLog::logMessage( errorString, QString::null, QgsMessageLog::CRITICAL ); + + } + + } + return keyTokens; } // makeKeyTokens_ diff --git a/src/core/qgsproject.h b/src/core/qgsproject.h index d7f27de2181..02273ce1f93 100644 --- a/src/core/qgsproject.h +++ b/src/core/qgsproject.h @@ -197,7 +197,7 @@ class CORE_EXPORT QgsProject : public QObject keys would be the familiar QSettings-like '/' delimited entries, implying a hierarchy of keys and corresponding values - @note The key string must include '/'s. E.g., "/foo" not "foo". + @note The key string must be valid xml tag names in order to be saved to the file. */ //@{ //! @note available in python bindings as writeEntryBool @@ -214,8 +214,6 @@ class CORE_EXPORT QgsProject : public QObject keys would be the familiar QSettings-like '/' delimited entries, implying a hierarchy of keys and corresponding values - - @note The key string must include '/'s. E.g., "/foo" not "foo". */ //@{ QStringList readListEntry( const QString & scope, const QString & key, const QStringList& def = QStringList(), bool *ok = 0 ) const; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 1af4d141f04..74777b7145a 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -30,6 +30,7 @@ ADD_PYTHON_TEST(PyQgsDistanceArea test_qgsdistancearea.py) ADD_PYTHON_TEST(PyQgsEditWidgets test_qgseditwidgets.py) ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py) ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py) +ADD_PYTHON_TEST(PyQgsProject test_qgsproject.py) ADD_PYTHON_TEST(PyQgsFeatureIterator test_qgsfeatureiterator.py) ADD_PYTHON_TEST(PyQgsField test_qgsfield.py) ADD_PYTHON_TEST(PyQgsFontUtils test_qgsfontutils.py) diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py new file mode 100644 index 00000000000..5dcbc2245ed --- /dev/null +++ b/tests/src/python/test_qgsproject.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsProject. + +.. note:: 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. +""" +__author__ = 'Sebastian Dietrich' +__date__ = '19/11/2015' +__copyright__ = 'Copyright 2015, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis + +from qgis.core import QgsProject, QgsMessageLog + +from utilities import getQgisTestApp, TestCase, unittest + +QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp() + + +class TestQgsProject(TestCase): + + def __init__(self, methodName): + """Run once on class initialisation.""" + unittest.TestCase.__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 = u"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 = u"+*,;<>|!$%()=?#\x01"; + + # generate the characters that are allowed at the start of a token (and at every other position) + validStartChars = u":_"; + charRanges = [ + (ord(u'a'), ord(u'z')), + (ord(u'A'), ord(u'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 += unichr(c) + + # generate the characters that are only allowed inside a token, not at the start + validInlineChars = u"-.\xB7"; + charRanges = [ + (ord(u'0'), ord(u'9')), + (0x0300, 0x036F), + (0x203F, 0x2040), + ] + for r in charRanges: + for c in range(r[0], r[1]): + validInlineChars += unichr(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 = QgsMessageLog.instance() + logger.messageReceived.connect(self.catchMessage) + prj = QgsProject.instance() + + for token in validTokens: + self.messageCaught = False + prj.readEntry("test", token) + myMessage = "valid token '%s' not accepted" % (token) + assert not self.messageCaught, myMessage + + for token in invalidTokens: + self.messageCaught = False + prj.readEntry("test", token) + myMessage = "invalid token '%s' accepted" % (token) + assert self.messageCaught, myMessage + + logger.messageReceived.disconnect(self.catchMessage) + + def catchMessage(self): + self.messageCaught = True + +if __name__ == '__main__': + unittest.main()