diff --git a/vcpkg/ports/qgis/portfile.cmake b/vcpkg/ports/qgis/portfile.cmake index 2a174883f..b335a3e56 100644 --- a/vcpkg/ports/qgis/portfile.cmake +++ b/vcpkg/ports/qgis/portfile.cmake @@ -16,6 +16,8 @@ vcpkg_from_github( cmakelists.patch crssync.patch libxml2.patch + qgis4-project-properties.patch + qgis4_url_encoding.patch ) file(REMOVE ${SOURCE_PATH}/cmake/FindQtKeychain.cmake) diff --git a/vcpkg/ports/qgis/qgis4-project-properties.patch b/vcpkg/ports/qgis/qgis4-project-properties.patch new file mode 100644 index 000000000..814fcf5b5 --- /dev/null +++ b/vcpkg/ports/qgis/qgis4-project-properties.patch @@ -0,0 +1,274 @@ +commit 74549aad26c3358101e88477d9dfa1caae013d72 +Author: Jürgen E. Fischer +Date: Fri Jun 20 15:58:30 2025 +0200 + + Reapply "Allow free naming of project properties (#60855)" + + This reverts commit fb11239112adfc321b3bbacbb20da888a7a37c23. + +diff --git a/src/core/project/qgsproject.cpp b/src/core/project/qgsproject.cpp +index f78f9e53bef..cd6f78edaaf 100644 +--- a/src/core/project/qgsproject.cpp ++++ b/src/core/project/qgsproject.cpp +@@ -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} +- * +- * +- * 42 +- * 1 +- * ++ * ++ * ++ * 42 ++ * 1 ++ * + * railroad + * airport +- * +- * 1 +- * 123.456 +- * ++ * ++ * 1 ++ * 123.456 ++ * + * type +- * +- * ++ * ++ * + * + * \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() ) ++ { ++ 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 = absElem.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0; ++ useAbsolutePaths = e.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0; + } + } + +diff --git a/src/core/project/qgsprojectproperty.cpp b/src/core/project/qgsprojectproperty.cpp +index ff8024a5260..1af598012b4 100644 +--- a/src/core/project/qgsprojectproperty.cpp ++++ b/src/core/project/qgsprojectproperty.cpp +@@ -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 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() ); +- +- QDomNode subkey = subkeys.item( i ); ++ addKey( name ); + +- 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() ) + { +diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py +index 237553260f6..d44d4438006 100644 +--- a/tests/src/python/test_qgsproject.py ++++ b/tests/src/python/test_qgsproject.py +@@ -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 + diff --git a/vcpkg/ports/qgis/qgis4_url_encoding.patch b/vcpkg/ports/qgis/qgis4_url_encoding.patch new file mode 100644 index 000000000..d67e50968 --- /dev/null +++ b/vcpkg/ports/qgis/qgis4_url_encoding.patch @@ -0,0 +1,847 @@ +diff --git a/src/core/network/qgshttpheaders.cpp b/src/core/network/qgshttpheaders.cpp +index 74322dc2fcb..c5cfbbabc66 100644 +--- a/src/core/network/qgshttpheaders.cpp ++++ b/src/core/network/qgshttpheaders.cpp +@@ -73,7 +73,7 @@ bool QgsHttpHeaders::updateUrlQuery( QUrlQuery &uri ) const + { + for ( auto ite = mHeaders.constBegin(); ite != mHeaders.constEnd(); ++ite ) + { +- uri.addQueryItem( QgsHttpHeaders::PARAM_PREFIX + ite.key().toUtf8(), ite.value().toString().toUtf8() ); ++ uri.addQueryItem( QgsHttpHeaders::PARAM_PREFIX + ite.key().toUtf8(), QUrl::toPercentEncoding( ite.value().toString() ) ); + } + return true; + } +diff --git a/src/core/project/qgsprojectstorageregistry.cpp b/src/core/project/qgsprojectstorageregistry.cpp +index a86c4d2bc60..f559bb21112 100644 +--- a/src/core/project/qgsprojectstorageregistry.cpp ++++ b/src/core/project/qgsprojectstorageregistry.cpp +@@ -33,8 +33,7 @@ QgsProjectStorage *QgsProjectStorageRegistry::projectStorageFromUri( const QStri + for ( auto it = mBackends.constBegin(); it != mBackends.constEnd(); ++it ) + { + QgsProjectStorage *storage = it.value(); +- const QString scheme = storage->type() + ':'; +- if ( uri.startsWith( scheme ) ) ++ if ( uri.startsWith( storage->type() + ':' ) || uri.startsWith( storage->type() + "%3A" ) ) + return storage; + } + +diff --git a/src/core/qgsdatasourceuri.cpp b/src/core/qgsdatasourceuri.cpp +index 6b26e9dd2ee..fe0dd2a12b5 100644 +--- a/src/core/qgsdatasourceuri.cpp ++++ b/src/core/qgsdatasourceuri.cpp +@@ -701,17 +701,17 @@ QByteArray QgsDataSourceUri::encodedUri() const + QUrlQuery url; + for ( auto it = mParams.constBegin(); it != mParams.constEnd(); ++it ) + { +- url.addQueryItem( it.key(), it.value() ); ++ url.addQueryItem( it.key(), QUrl::toPercentEncoding( it.value() ) ); + } + + if ( !mUsername.isEmpty() ) +- url.addQueryItem( QStringLiteral( "username" ), mUsername ); ++ url.addQueryItem( QStringLiteral( "username" ), QUrl::toPercentEncoding( mUsername ) ); + + if ( !mPassword.isEmpty() ) +- url.addQueryItem( QStringLiteral( "password" ), mPassword ); ++ url.addQueryItem( QStringLiteral( "password" ), QUrl::toPercentEncoding( mPassword ) ); + + if ( !mAuthConfigId.isEmpty() ) +- url.addQueryItem( QStringLiteral( "authcfg" ), mAuthConfigId ); ++ url.addQueryItem( QStringLiteral( "authcfg" ), QUrl::toPercentEncoding( mAuthConfigId ) ); + + mHttpHeaders.updateUrlQuery( url ); + +@@ -731,7 +731,7 @@ void QgsDataSourceUri::setEncodedUri( const QByteArray &uri ) + + mHttpHeaders.setFromUrlQuery( query ); + +- const auto constQueryItems = query.queryItems(); ++ const auto constQueryItems = query.queryItems( QUrl::ComponentFormattingOption::FullyDecoded ); + for ( const QPair &item : constQueryItems ) + { + if ( !item.first.startsWith( QgsHttpHeaders::PARAM_PREFIX ) && item.first != QgsHttpHeaders::KEY_REFERER ) +diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp +index 90b7fba14bb..c2e3cc126bb 100644 +--- a/src/core/qgsmaplayer.cpp ++++ b/src/core/qgsmaplayer.cpp +@@ -3262,8 +3262,9 @@ QString QgsMapLayer::generalHtmlMetadata() const + } + if ( uriComponents.contains( QStringLiteral( "url" ) ) ) + { +- const QString url = uriComponents[QStringLiteral( "url" )].toString(); +- metadata += QStringLiteral( "" ) + tr( "URL" ) + QStringLiteral( "%1" ).arg( QStringLiteral( "%2" ).arg( QUrl( url ).toString(), url ) ) + QStringLiteral( "\n" ); ++ QUrl decodedUri = QUrl::fromPercentEncoding( uriComponents[QStringLiteral( "url" )].toString().toLocal8Bit() ); ++ const QString url = decodedUri.toString(); ++ metadata += QStringLiteral( "" ) + tr( "URL" ) + QStringLiteral( "%1" ).arg( QStringLiteral( "%2" ).arg( url, url ) ) + QStringLiteral( "\n" ); + } + } + +diff --git a/src/core/vectortile/qgsvectortileprovidermetadata.cpp b/src/core/vectortile/qgsvectortileprovidermetadata.cpp +index f7a8b5f1fd9..a6484adde6a 100644 +--- a/src/core/vectortile/qgsvectortileprovidermetadata.cpp ++++ b/src/core/vectortile/qgsvectortileprovidermetadata.cpp +@@ -147,7 +147,7 @@ QString QgsVectorTileProviderMetadata::absoluteToRelativeUri( const QString &uri + // relative path will become "file:./x.txt" + const QString relSrcUrl = context.pathResolver().writePath( sourceUrl.toLocalFile() ); + dsUri.removeParam( QStringLiteral( "url" ) ); // needed because setParam() would insert second "url" key +- dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( relSrcUrl ).toString() ); ++ dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( relSrcUrl ).toString( QUrl::DecodeReserved ) ); + return dsUri.encodedUri(); + } + } +@@ -176,7 +176,7 @@ QString QgsVectorTileProviderMetadata::relativeToAbsoluteUri( const QString &uri + { + const QString absSrcUrl = context.pathResolver().readPath( sourceUrl.toLocalFile() ); + dsUri.removeParam( QStringLiteral( "url" ) ); // needed because setParam() would insert second "url" key +- dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( absSrcUrl ).toString() ); ++ dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( absSrcUrl ).toString( QUrl::DecodeReserved ) ); + return dsUri.encodedUri(); + } + } +diff --git a/src/core/vectortile/qgsxyzvectortiledataprovider.cpp b/src/core/vectortile/qgsxyzvectortiledataprovider.cpp +index be607514666..08c45dbe3c5 100644 +--- a/src/core/vectortile/qgsxyzvectortiledataprovider.cpp ++++ b/src/core/vectortile/qgsxyzvectortiledataprovider.cpp +@@ -316,7 +316,7 @@ QString QgsXyzVectorTileDataProviderMetadata::absoluteToRelativeUri( const QStri + // relative path will become "file:./x.txt" + const QString relSrcUrl = context.pathResolver().writePath( sourceUrl.toLocalFile() ); + dsUri.removeParam( QStringLiteral( "url" ) ); // needed because setParam() would insert second "url" key +- dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( relSrcUrl ).toString() ); ++ dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( relSrcUrl ).toString( QUrl::DecodeReserved ) ); + return dsUri.encodedUri(); + } + +@@ -335,7 +335,7 @@ QString QgsXyzVectorTileDataProviderMetadata::relativeToAbsoluteUri( const QStri + { + const QString absSrcUrl = context.pathResolver().readPath( sourceUrl.toLocalFile() ); + dsUri.removeParam( QStringLiteral( "url" ) ); // needed because setParam() would insert second "url" key +- dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( absSrcUrl ).toString() ); ++ dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( absSrcUrl ).toString( QUrl::DecodeReserved ) ); + return dsUri.encodedUri(); + } + +diff --git a/tests/src/app/testqgsidentify.cpp b/tests/src/app/testqgsidentify.cpp +index 15aaec87c9d..65fe2e81bc0 100644 +--- a/tests/src/app/testqgsidentify.cpp ++++ b/tests/src/app/testqgsidentify.cpp +@@ -933,7 +933,9 @@ void TestQgsIdentify::identifyVectorTile() + const QString vtPath = QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/vector_tile/{z}-{x}-{y}.pbf" ); + QgsDataSourceUri dsUri; + dsUri.setParam( QStringLiteral( "type" ), QStringLiteral( "xyz" ) ); +- dsUri.setParam( QStringLiteral( "url" ), QUrl::fromLocalFile( vtPath ).toString() ); ++ // The values need to be passed to QgsDataSourceUri::setParam() in the same format they are expected to be retrieved. ++ // QUrl::fromPercentEncoding() is needed here because QUrl::fromLocalFile(vtPath).toString() returns the curly braces in an URL-encoded format. ++ dsUri.setParam( QStringLiteral( "url" ), QUrl::fromPercentEncoding( QUrl::fromLocalFile( vtPath ).toString().toUtf8() ) ); + QgsVectorTileLayer *tempLayer = new QgsVectorTileLayer( dsUri.encodedUri(), QStringLiteral( "testlayer" ) ); + QVERIFY( tempLayer->isValid() ); + +diff --git a/tests/src/core/testqgsdatasourceuri.cpp b/tests/src/core/testqgsdatasourceuri.cpp +index f7889423245..4eaac93f4bd 100644 +--- a/tests/src/core/testqgsdatasourceuri.cpp ++++ b/tests/src/core/testqgsdatasourceuri.cpp +@@ -38,6 +38,7 @@ class TestQgsDataSourceUri : public QObject + void checkParameterKeys(); + void checkRemovePassword(); + void checkUnicodeUri(); ++ void checkUriInUri(); + }; + + void TestQgsDataSourceUri::checkparser_data() +@@ -775,7 +776,7 @@ void TestQgsDataSourceUri::checkAuthParams() + // issue GH #53654 + QgsDataSourceUri uri5; + uri5.setEncodedUri( QStringLiteral( "zmax=14&zmin=0&styleUrl=http://localhost:8000/&f=application%2Fvnd.geoserver.mbstyle%2Bjson" ) ); +- QCOMPARE( uri5.param( QStringLiteral( "f" ) ), QStringLiteral( "application%2Fvnd.geoserver.mbstyle%2Bjson" ) ); ++ QCOMPARE( uri5.param( QStringLiteral( "f" ) ), QStringLiteral( "application/vnd.geoserver.mbstyle+json" ) ); + + uri5.setEncodedUri( QStringLiteral( "zmax=14&zmin=0&styleUrl=http://localhost:8000/&f=application/vnd.geoserver.mbstyle+json" ) ); + QCOMPARE( uri5.param( QStringLiteral( "f" ) ), QStringLiteral( "application/vnd.geoserver.mbstyle+json" ) ); +@@ -822,6 +823,83 @@ void TestQgsDataSourceUri::checkUnicodeUri() + QCOMPARE( uri.param( QStringLiteral( "url" ) ), QStringLiteral( "file:///directory/テスト.mbtiles" ) ); + } + ++void TestQgsDataSourceUri::checkUriInUri() ++{ ++ QString dataUri = QStringLiteral( "dpiMode=7&url=%1&SERVICE=WMS&REQUEST=GetCapabilities&username=username&password=qgis%C3%A8%C3%A9" ); ++ ++ // If the 'url' field references a QGIS server then the 'MAP' parameter can contain an url to the project file. ++ // When the project is saved in a postgresql db, the connection url will also contains '&' and '='. ++ { ++ QgsDataSourceUri uri; ++ // here the project url is encoded but the whole serverUrl is not encoded. ++ // The OGC server will receive a call with this url: http://localhost:8000/ows/?MAP=postgresql://?service=qgis_test&dbname&schema=project&project=luxembourg&SERVICE=WMS&REQUEST=GetCapabilities ++ // from the OGC server POV the 'schema' and 'project' keys will be parsed as main query parameters for 'http://localhost:8000/ows/?' ++ // and not associated to the project file uri. ++ QString project = "postgresql://?service=qgis_test&dbname&schema=project&project=luxembourg"; ++ QString projectEnc = QUrl::toPercentEncoding( project ); ++ QString serverUrl = QString( "http://localhost:8000/ows/?MAP=%1" ); ++ uri.setEncodedUri( dataUri.arg( serverUrl.arg( projectEnc ) ) ); ++ QCOMPARE( uri.param( QStringLiteral( "username" ) ), QStringLiteral( "username" ) ); ++ QCOMPARE( uri.username(), QStringLiteral( "username" ) ); ++ QCOMPARE( uri.param( QStringLiteral( "password" ) ), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri.password(), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri.param( QStringLiteral( "SERVICE" ) ), QStringLiteral( "WMS" ) ); ++ QCOMPARE( uri.param( QStringLiteral( "REQUEST" ) ), QStringLiteral( "GetCapabilities" ) ); ++ // not enough encoded at the beginning ==> bad encoding at the end ++ QCOMPARE( uri.param( QStringLiteral( "url" ) ), serverUrl.arg( project ) ); ++ ++ QgsDataSourceUri uri2; ++ // here the project url is encoded and the whole serverUrl is also encoded. ++ // The OGC server will receive a call with this url: http://localhost:8000/ows/?MAP=postgresql%3A%2F%2F%3Fservice%3Dqgis_test%26dbname%26schema%3Dproject%26project%3Dluxembourg&SERVICE=WMS&REQUEST=GetCapabilities ++ // and will be able to decode all parameters ++ QString serverUrlEnc = QUrl::toPercentEncoding( serverUrl.arg( projectEnc ) ); ++ uri2.setEncodedUri( dataUri.arg( serverUrlEnc ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "username" ) ), QStringLiteral( "username" ) ); ++ QCOMPARE( uri2.username(), QStringLiteral( "username" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "password" ) ), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri2.password(), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "SERVICE" ) ), QStringLiteral( "WMS" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "REQUEST" ) ), QStringLiteral( "GetCapabilities" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "url" ) ), serverUrl.arg( projectEnc ) ); ++ } ++ ++ // same as above but with extra param at the end of the ++ { ++ QgsDataSourceUri uri; ++ // here the project url is encoded but the whole serverUrl is not encoded. ++ // The OGC server will receive a call with this url: https://titiler.xyz/cog/tiles/WebMercatorQuad/16/34060/23336@1x?url=https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif&bidx=1&rescale=1600%2C2100&colormap_name=gist_earth ++ // from the OGC server POV the 'rescale' and 'colormap_name' keys could be parsed as sub query parameters for 'https://data.geo.admin.ch/' ++ QString project = "https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif"; ++ QString projectEnc = QUrl::toPercentEncoding( project ); ++ QString extraParam = "&bidx=1&rescale=1600%2C2100&colormap_name=gist_earth"; ++ QString serverUrl = QString( "https://titiler.xyz/cog/tiles/WebMercatorQuad/16/34060/23336@1x?url=%1" ); ++ ++ uri.setEncodedUri( dataUri.arg( serverUrl.arg( projectEnc ) + extraParam ) ); ++ QCOMPARE( uri.param( QStringLiteral( "username" ) ), QStringLiteral( "username" ) ); ++ QCOMPARE( uri.username(), QStringLiteral( "username" ) ); ++ QCOMPARE( uri.param( QStringLiteral( "password" ) ), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri.password(), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri.param( QStringLiteral( "SERVICE" ) ), QStringLiteral( "WMS" ) ); ++ QCOMPARE( uri.param( QStringLiteral( "REQUEST" ) ), QStringLiteral( "GetCapabilities" ) ); ++ // not enough encoded at the beginning ==> bad encoding at the end ++ QCOMPARE( uri.param( QStringLiteral( "url" ) ), serverUrl.arg( project ) ); ++ ++ QgsDataSourceUri uri2; ++ // here the project url is encoded and the whole serverUrl is also encoded. ++ // The OGC server will receive a call with this url: https://titiler.xyz/cog/tiles/WebMercatorQuad/16/34060/23336@1x?url=https%3A%2F%2Fdata.geo.admin.ch%2Fch.swisstopo.swissalti3d%2Fswissalti3d_2019_2573-1085%2Fswissalti3d_2019_2573-1085_0.5_2056_5728.tif&bidx=1&rescale=1600%2C2100&colormap_name=gist_earth ++ // and will be able to decode all parameters ++ QString serverUrlEnc = QUrl::toPercentEncoding( serverUrl.arg( projectEnc ) + extraParam ); ++ uri2.setEncodedUri( dataUri.arg( serverUrlEnc ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "username" ) ), QStringLiteral( "username" ) ); ++ QCOMPARE( uri2.username(), QStringLiteral( "username" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "password" ) ), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri2.password(), QStringLiteral( "qgisèé" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "SERVICE" ) ), QStringLiteral( "WMS" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "REQUEST" ) ), QStringLiteral( "GetCapabilities" ) ); ++ QCOMPARE( uri2.param( QStringLiteral( "url" ) ), serverUrl.arg( projectEnc ) + extraParam ); ++ } ++} ++ + + QGSTEST_MAIN( TestQgsDataSourceUri ) + #include "testqgsdatasourceuri.moc" +diff --git a/tests/src/core/testqgsgdalcloudconnection.cpp b/tests/src/core/testqgsgdalcloudconnection.cpp +index e43c4757ee7..0e69eb210ab 100644 +--- a/tests/src/core/testqgsgdalcloudconnection.cpp ++++ b/tests/src/core/testqgsgdalcloudconnection.cpp +@@ -59,7 +59,7 @@ void TestQgsGdalCloudConnection::encodeDecode() + data.rootPath = QStringLiteral( "some/path" ); + data.credentialOptions = QVariantMap { { "pw", QStringLiteral( "xxxx" ) }, { "key", QStringLiteral( "yyy" ) } }; + +- QCOMPARE( QgsGdalCloudProviderConnection::encodedUri( data ), QStringLiteral( "container=my_container&credentialOptions=key%3Dyyy%7Cpw%3Dxxxx&handler=vsis3&rootPath=some/path" ) ); ++ QCOMPARE( QgsGdalCloudProviderConnection::encodedUri( data ), QStringLiteral( "container=my_container&credentialOptions=key%3Dyyy%7Cpw%3Dxxxx&handler=vsis3&rootPath=some%2Fpath" ) ); + + const QgsGdalCloudProviderConnection::Data data2 = QgsGdalCloudProviderConnection::decodedUri( QStringLiteral( "container=my_container&credentialOptions=key%3Dyyy%7Cpw%3Dxxxx&handler=vsis3&rootPath=some/path" ) ); + QCOMPARE( data2.vsiHandler, QStringLiteral( "vsis3" ) ); +@@ -94,7 +94,7 @@ void TestQgsGdalCloudConnection::testConnections() + + // retrieve stored connection + conn = QgsGdalCloudProviderConnection( QStringLiteral( "my connection" ) ); +- QCOMPARE( conn.uri(), QStringLiteral( "container=my_container&credentialOptions=key%3Dyyy%7Cpw%3Dxxxx&handler=vsis3&rootPath=some/path" ) ); ++ QCOMPARE( conn.uri(), QStringLiteral( "container=my_container&credentialOptions=key%3Dyyy%7Cpw%3Dxxxx&handler=vsis3&rootPath=some%2Fpath" ) ); + + // add a second connection + QgsGdalCloudProviderConnection::Data data2; +diff --git a/tests/src/core/testqgshttpheaders.cpp b/tests/src/core/testqgshttpheaders.cpp +index 867e44de619..623e9532dbb 100644 +--- a/tests/src/core/testqgshttpheaders.cpp ++++ b/tests/src/core/testqgshttpheaders.cpp +@@ -187,11 +187,14 @@ void TestQgsHttpheaders::createQgsOwsConnection() + + QgsOwsConnection ows( "service", "name" ); + QCOMPARE( ows.connectionInfo(), ",authcfg=,referer=http://test.com" ); +- QCOMPARE( ows.uri().encodedUri(), "url&http-header:other_http_header=value&http-header:referer=http://test.com" ); ++ if ( ows.uri().encodedUri().startsWith( "url=" ) ) ++ QCOMPARE( ows.uri().encodedUri(), "url=&http-header:other_http_header=value&http-header:referer=http%3A%2F%2Ftest.com" ); ++ else ++ QCOMPARE( ows.uri().encodedUri(), "url&http-header:other_http_header=value&http-header:referer=http%3A%2F%2Ftest.com" ); + + QgsDataSourceUri uri( QString( "https://www.ogc.org/?p1=v1" ) ); + QgsDataSourceUri uri2 = ows.addWmsWcsConnectionSettings( uri, "service", "name" ); +- QCOMPARE( uri2.encodedUri(), "https://www.ogc.org/?p1=v1&http-header:other_http_header=value&http-header:referer=http://test.com" ); ++ QCOMPARE( uri2.encodedUri(), "https://www.ogc.org/?p1=v1&http-header:other_http_header=value&http-header:referer=http%3A%2F%2Ftest.com" ); + + // check space separated string + QCOMPARE( uri2.uri(), " https://www.ogc.org/?p1='v1' http-header:other_http_header='value' http-header:referer='http://test.com' referer='http://test.com'" ); +@@ -199,7 +202,7 @@ void TestQgsHttpheaders::createQgsOwsConnection() + QgsDataSourceUri uri3( uri2.uri() ); + QCOMPARE( uri3.httpHeader( QgsHttpHeaders::KEY_REFERER ), "http://test.com" ); + QCOMPARE( uri3.httpHeader( "other_http_header" ), "value" ); +- QCOMPARE( uri3.encodedUri(), "https://www.ogc.org/?p1=v1&referer=http://test.com&http-header:other_http_header=value&http-header:referer=http://test.com" ); ++ QCOMPARE( uri3.encodedUri(), "https://www.ogc.org/?p1=v1&referer=http%3A%2F%2Ftest.com&http-header:other_http_header=value&http-header:referer=http%3A%2F%2Ftest.com" ); + } + + +diff --git a/tests/src/core/testqgsmaplayer.cpp b/tests/src/core/testqgsmaplayer.cpp +index b4a99607aa0..90969a5f0a4 100644 +--- a/tests/src/core/testqgsmaplayer.cpp ++++ b/tests/src/core/testqgsmaplayer.cpp +@@ -33,6 +33,7 @@ + #include "qgsmaplayerstore.h" + #include "qgsproject.h" + #include "qgsxmlutils.h" ++#include "qgsvectortilelayer.h" + + /** + * \ingroup UnitTests +@@ -55,6 +56,8 @@ class TestQgsMapLayer : public QObject + void testId(); + void formatName(); + ++ void generalHtmlMetadata(); ++ + void setBlendMode(); + + void isInScaleRange_data(); +@@ -153,6 +156,33 @@ void TestQgsMapLayer::testId() + QCOMPARE( spy3.count(), 1 ); + } + ++void TestQgsMapLayer::generalHtmlMetadata() ++{ ++ { ++ QgsDataSourceUri ds; ++ ds.setParam( QStringLiteral( "type" ), "xyz" ); ++ ds.setParam( QStringLiteral( "zmax" ), "1" ); ++ ds.setParam( QStringLiteral( "url" ), "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png" ); ++ std::unique_ptr vl( new QgsVectorTileLayer( ds.encodedUri(), QStringLiteral( "testLayer" ) ) ); ++ QVERIFY( vl->dataProvider() ); ++ QVERIFY( vl->dataProvider()->isValid() ); ++ QCOMPARE( ds.param( QStringLiteral( "url" ) ), "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png" ); ++ QVERIFY( vl->generalHtmlMetadata().contains( "URL vl( new QgsVectorTileLayer( ds.encodedUri(), QStringLiteral( "testLayer" ) ) ); ++ QVERIFY( vl->dataProvider() ); ++ QVERIFY( vl->dataProvider()->isValid() ); ++ QCOMPARE( ds.param( QStringLiteral( "url" ) ), QStringLiteral( "%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QVERIFY( vl->generalHtmlMetadata().contains( QStringLiteral( "Path( &conn )->providerKey(), QStringLiteral( "test_provider" ) ); + + // add a second connection +@@ -110,7 +110,7 @@ void TestQgsTiledSceneConnection::testConnections() + data2.httpHeaders.insert( QStringLiteral( "my_header" ), QStringLiteral( "value2" ) ); + // construct connection using encoded uri + QgsTiledSceneProviderConnection conn2( QgsTiledSceneProviderConnection::encodedUri( data2 ), QStringLiteral( "test_provider2" ), {} ); +- QCOMPARE( conn2.uri(), QStringLiteral( "url=http://testurl2&username=my_user2&password=my_pw2&authcfg=my_auth2&http-header:my_header=value2" ) ); ++ QCOMPARE( conn2.uri(), QStringLiteral( "url=http%3A%2F%2Ftesturl2&username=my_user2&password=my_pw2&authcfg=my_auth2&http-header:my_header=value2" ) ); + QCOMPARE( qgis::down_cast( &conn2 )->providerKey(), QStringLiteral( "test_provider2" ) ); + conn2.store( QStringLiteral( "second connection" ) ); + +diff --git a/tests/src/core/testqgsvectortileconnection.cpp b/tests/src/core/testqgsvectortileconnection.cpp +index e539eb0be69..d73454fa428 100644 +--- a/tests/src/core/testqgsvectortileconnection.cpp ++++ b/tests/src/core/testqgsvectortileconnection.cpp +@@ -62,13 +62,13 @@ void TestQgsVectorTileConnection::test_encodedUri() + conn.zMin = 0; + conn.zMax = 18; + QString uri = QgsVectorTileProviderConnection::encodedUri( conn ); +- QCOMPARE( uri, QStringLiteral( "type=xyz&url=https://api.maptiler.com/tiles/v3/%7Bz%7D/%7Bx%7D/%7By%7D.pbf?key%3Dabcdef12345&zmax=18&zmin=0" ) ); ++ QCOMPARE( uri, QStringLiteral( "type=xyz&url=https%3A%2F%2Fapi.maptiler.com%2Ftiles%2Fv3%2F%7Bz%7D%2F%7Bx%7D%2F%7By%7D.pbf%3Fkey%3Dabcdef12345&zmax=18&zmin=0" ) ); + + conn.url = QStringLiteral( "file:///home/user/tiles.mbtiles" ); + conn.zMin = 0; + conn.zMax = 18; + uri = QgsVectorTileProviderConnection::encodedUri( conn ); +- QCOMPARE( uri, QStringLiteral( "type=mbtiles&url=file:///home/user/tiles.mbtiles&zmax=18&zmin=0" ) ); ++ QCOMPARE( uri, QStringLiteral( "type=mbtiles&url=file%3A%2F%2F%2Fhome%2Fuser%2Ftiles.mbtiles&zmax=18&zmin=0" ) ); + } + + +diff --git a/tests/src/core/testqgsvectortilelayer.cpp b/tests/src/core/testqgsvectortilelayer.cpp +index 8248b48f4b5..b9b0ec9ef9f 100644 +--- a/tests/src/core/testqgsvectortilelayer.cpp ++++ b/tests/src/core/testqgsvectortilelayer.cpp +@@ -261,11 +261,12 @@ void TestQgsVectorTileLayer::testMbtilesProviderMetadata() + QCOMPARE( vectorTileMetadata->validLayerTypesForUri( QStringLiteral( "type=mbtiles&url=%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ) ), { Qgis::LayerType::VectorTile } ); + + // query sublayers ++ QString localMbtilesPath = QStringLiteral( "%1%2" ).arg( QUrl::toPercentEncoding( TEST_DATA_DIR ), QUrl::toPercentEncoding( QStringLiteral( "/vector_tile/mbtiles_vt.mbtiles" ) ) ); + QList sublayers = vectorTileMetadata->querySublayers( QStringLiteral( "%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ) ); + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "mbtilesvectortiles" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "mbtiles_vt" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1" ).arg( localMbtilesPath ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); + QVERIFY( !sublayers.at( 0 ).skippedContainerScan() ); + QVERIFY( !QgsProviderUtils::sublayerDetailsAreIncomplete( sublayers ) ); +@@ -274,7 +275,7 @@ void TestQgsVectorTileLayer::testMbtilesProviderMetadata() + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "mbtilesvectortiles" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "mbtiles_vt" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1" ).arg( localMbtilesPath ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); + QVERIFY( !sublayers.at( 0 ).skippedContainerScan() ); + +@@ -283,7 +284,7 @@ void TestQgsVectorTileLayer::testMbtilesProviderMetadata() + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "mbtilesvectortiles" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "mbtiles_vt" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1" ).arg( localMbtilesPath ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); + QVERIFY( sublayers.at( 0 ).skippedContainerScan() ); + QVERIFY( QgsProviderUtils::sublayerDetailsAreIncomplete( sublayers ) ); +@@ -292,17 +293,19 @@ void TestQgsVectorTileLayer::testMbtilesProviderMetadata() + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "mbtilesvectortiles" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "mbtiles_vt" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1" ).arg( localMbtilesPath ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); + QVERIFY( sublayers.at( 0 ).skippedContainerScan() ); + + // fast scan mode means that any mbtile file will be reported, including those with only raster tiles + // (we are skipping a potentially expensive db open and format check) ++ QString localIsleOfManPath = QStringLiteral( "%1%2" ).arg( QUrl::toPercentEncoding( TEST_DATA_DIR ), QUrl::toPercentEncoding( QStringLiteral( "/isle_of_man.mbtiles" ) ) ); ++ + sublayers = vectorTileMetadata->querySublayers( QStringLiteral( "%1/isle_of_man.mbtiles" ).arg( TEST_DATA_DIR ), Qgis::SublayerQueryFlag::FastScan ); + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "mbtilesvectortiles" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "isle_of_man" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1/isle_of_man.mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=mbtiles&url=%1" ).arg( localIsleOfManPath ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); + QVERIFY( sublayers.at( 0 ).skippedContainerScan() ); + +@@ -333,8 +336,9 @@ void TestQgsVectorTileLayer::test_relativePathsMbTiles() + QgsReadWriteContext contextRel; + contextRel.setPathResolver( QgsPathResolver( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/project.qgs" ) ) ); + const QgsReadWriteContext contextAbs; ++ QString localMbtilesPath = QStringLiteral( "%1%2" ).arg( QUrl::toPercentEncoding( TEST_DATA_DIR ), QUrl::toPercentEncoding( QStringLiteral( "/vector_tile/mbtiles_vt.mbtiles" ) ) ); + +- const QString srcMbtiles = QStringLiteral( "type=mbtiles&url=%1/vector_tile/mbtiles_vt.mbtiles" ).arg( TEST_DATA_DIR ); ++ const QString srcMbtiles = QStringLiteral( "type=mbtiles&url=%1" ).arg( localMbtilesPath ); + + std::unique_ptr layer = std::make_unique( srcMbtiles ); + QVERIFY( layer->isValid() ); +@@ -342,7 +346,7 @@ void TestQgsVectorTileLayer::test_relativePathsMbTiles() + + // encode source: converting absolute paths to relative + const QString srcMbtilesRel = layer->encodedSource( srcMbtiles, contextRel ); +- QCOMPARE( srcMbtilesRel, QStringLiteral( "type=mbtiles&url=./vector_tile/mbtiles_vt.mbtiles" ) ); ++ QCOMPARE( srcMbtilesRel, QStringLiteral( "type=mbtiles&url=.%2Fvector_tile%2Fmbtiles_vt.mbtiles" ) ); + + // encode source: keeping absolute paths + QCOMPARE( layer->encodedSource( srcMbtiles, contextAbs ), srcMbtiles ); +@@ -393,15 +397,15 @@ void TestQgsVectorTileLayer::test_relativePathsXyz() + contextRel.setPathResolver( QgsPathResolver( "/home/qgis/project.qgs" ) ); + const QgsReadWriteContext contextAbs; + +- const QString srcXyzLocal = "type=xyz&url=file:///home/qgis/%7Bz%7D/%7Bx%7D/%7By%7D.pbf"; +- const QString srcXyzRemote = "type=xyz&url=http://www.example.com/%7Bz%7D/%7Bx%7D/%7By%7D.pbf"; ++ const QString srcXyzLocal = "type=xyz&url=file%3A%2F%2F%2Fhome%2Fqgis%2F%7Bz%7D%2F%7Bx%7D%2F%7By%7D.pbf"; ++ const QString srcXyzRemote = "type=xyz&url=http%3A%2F%2Fwww.example.com%2F%7Bz%7D%2F%7Bx%7D%2F%7By%7D.pbf"; + + std::unique_ptr layer = std::make_unique( srcXyzLocal ); + QCOMPARE( layer->providerType(), QStringLiteral( "xyzvectortiles" ) ); + + // encode source: converting absolute paths to relative + const QString srcXyzLocalRel = layer->encodedSource( srcXyzLocal, contextRel ); +- QCOMPARE( srcXyzLocalRel, QStringLiteral( "type=xyz&url=file:./%7Bz%7D/%7Bx%7D/%7By%7D.pbf" ) ); ++ QCOMPARE( srcXyzLocalRel, QStringLiteral( "type=xyz&url=file%3A.%2F%7Bz%7D%2F%7Bx%7D%2F%7By%7D.pbf" ) ); + QCOMPARE( layer->encodedSource( srcXyzRemote, contextRel ), srcXyzRemote ); + + // encode source: keeping absolute paths +@@ -437,7 +441,8 @@ void TestQgsVectorTileLayer::test_absoluteRelativeUriXyz() + + QString absoluteUri = dsAbs.encodedUri(); + QString relativeUri = dsRel.encodedUri(); +- QCOMPARE( vectorTileMetadata->absoluteToRelativeUri( absoluteUri, context ), relativeUri ); ++ QString absToRelUri = vectorTileMetadata->absoluteToRelativeUri( absoluteUri, context ); ++ QCOMPARE( absToRelUri, relativeUri ); + QCOMPARE( vectorTileMetadata->relativeToAbsoluteUri( relativeUri, context ), absoluteUri ); + } + +@@ -459,23 +464,23 @@ void TestQgsVectorTileLayer::testVtpkProviderMetadata() + QVERIFY( vectorTileMetadata->querySublayers( QStringLiteral( "type=vtpk&url=%1/points.shp" ).arg( TEST_DATA_DIR ) ).isEmpty() ); + + // vtpk uris +- QCOMPARE( vectorTileMetadata->priorityForUri( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/testvtpk.vtpk" ) ), 100 ); +- QCOMPARE( vectorTileMetadata->validLayerTypesForUri( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/testvtpk.vtpk" ) ), { Qgis::LayerType::VectorTile } ); +- QList sublayers = vectorTileMetadata->querySublayers( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/testvtpk.vtpk" ) ); +- QCOMPARE( sublayers.size(), 1 ); +- QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "vtpkvectortiles" ) ); +- QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "testvtpk" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=vtpk&url=%1/testvtpk.vtpk" ).arg( TEST_DATA_DIR ) ); +- QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); +- +- QCOMPARE( vectorTileMetadata->priorityForUri( QStringLiteral( "type=vtpk&url=%1/testvtpk.vtpk" ).arg( TEST_DATA_DIR ) ), 100 ); +- QCOMPARE( vectorTileMetadata->validLayerTypesForUri( QStringLiteral( "type=vtpk&url=%1/testvtpk.vtpk" ).arg( TEST_DATA_DIR ) ), { Qgis::LayerType::VectorTile } ); +- sublayers = vectorTileMetadata->querySublayers( QStringLiteral( "type=vtpk&url=%1/testvtpk.vtpk" ).arg( TEST_DATA_DIR ) ); +- QCOMPARE( sublayers.size(), 1 ); +- QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "vtpkvectortiles" ) ); +- QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "testvtpk" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=vtpk&url=%1/testvtpk.vtpk" ).arg( TEST_DATA_DIR ) ); +- QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); ++ QString localVtpkPath = QStringLiteral( "%1%2" ).arg( QUrl::toPercentEncoding( TEST_DATA_DIR ), QUrl::toPercentEncoding( QStringLiteral( "/testvtpk.vtpk" ) ) ); ++ ++ for ( auto uriStr : { ++ QStringLiteral( "%1/%2" ).arg( TEST_DATA_DIR ).arg( "testvtpk.vtpk" ), // ++ QStringLiteral( "type=vtpk&url=%1" ).arg( localVtpkPath ), // ++ QStringLiteral( "type=vtpk&url=%1" ).arg( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/testvtpk.vtpk" ) ) ++ } ) ++ { ++ QCOMPARE( vectorTileMetadata->priorityForUri( uriStr ), 100 ); ++ QCOMPARE( vectorTileMetadata->validLayerTypesForUri( uriStr ), { Qgis::LayerType::VectorTile } ); ++ QList sublayers = vectorTileMetadata->querySublayers( uriStr ); ++ QCOMPARE( sublayers.size(), 1 ); ++ QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "vtpkvectortiles" ) ); ++ QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "testvtpk" ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "type=vtpk&url=%1" ).arg( localVtpkPath ) ); ++ QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::VectorTile ); ++ } + + // test that vtpk provider is the preferred provider for vtpk files + QList candidates = QgsProviderRegistry::instance()->preferredProvidersForUri( QStringLiteral( "type=vtpk&url=%1/testvtpk.vtpk" ).arg( TEST_DATA_DIR ) ); +@@ -501,7 +506,9 @@ void TestQgsVectorTileLayer::test_relativePathsVtpk() + contextRel.setPathResolver( QgsPathResolver( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/project.qgs" ) ) ); + const QgsReadWriteContext contextAbs; + +- const QString srcVtpk = QStringLiteral( "type=vtpk&url=%1/testvtpk.vtpk" ).arg( TEST_DATA_DIR ); ++ QString localVtpkPath = QStringLiteral( "%1%2" ).arg( QUrl::toPercentEncoding( TEST_DATA_DIR ), QUrl::toPercentEncoding( QStringLiteral( "/testvtpk.vtpk" ) ) ); ++ ++ const QString srcVtpk = QStringLiteral( "type=vtpk&url=%1" ).arg( localVtpkPath ); + + std::unique_ptr layer = std::make_unique( srcVtpk ); + QVERIFY( layer->isValid() ); +@@ -509,7 +516,7 @@ void TestQgsVectorTileLayer::test_relativePathsVtpk() + + // encode source: converting absolute paths to relative + const QString srcVtpkRel = layer->encodedSource( srcVtpk, contextRel ); +- QCOMPARE( srcVtpkRel, QStringLiteral( "type=vtpk&url=./testvtpk.vtpk" ) ); ++ QCOMPARE( srcVtpkRel, QStringLiteral( "type=vtpk&url=.%2Ftestvtpk.vtpk" ) ); + + // encode source: keeping absolute paths + QCOMPARE( layer->encodedSource( srcVtpk, contextAbs ), srcVtpk ); +diff --git a/tests/src/providers/testqgswmsprovider.cpp b/tests/src/providers/testqgswmsprovider.cpp +index 8fe106aab19..e86d96fe894 100644 +--- a/tests/src/providers/testqgswmsprovider.cpp ++++ b/tests/src/providers/testqgswmsprovider.cpp +@@ -469,10 +469,27 @@ void TestQgsWmsProvider::absoluteRelativeUri() + QgsProviderMetadata *wmsMetadata = QgsProviderRegistry::instance()->providerMetadata( "wms" ); + QVERIFY( wmsMetadata ); + +- QString absoluteUri = "type=mbtiles&url=file://" + QStringLiteral( TEST_DATA_DIR ) + "/isle_of_man.mbtiles"; +- QString relativeUri = "type=mbtiles&url=file:./isle_of_man.mbtiles"; +- QCOMPARE( wmsMetadata->absoluteToRelativeUri( absoluteUri, context ), relativeUri ); +- QCOMPARE( wmsMetadata->relativeToAbsoluteUri( relativeUri, context ), absoluteUri ); ++ // from no encoded absolute url to encoded relative url ++ { ++ QString absoluteUri = QString( "type=mbtiles&url=" ) + "file://" + QStringLiteral( TEST_DATA_DIR ) + "/isle_of_man.mbtiles"; ++ QString relativeUri = "type=mbtiles&url=file%3A.%2Fisle_of_man.mbtiles"; ++ QCOMPARE( wmsMetadata->absoluteToRelativeUri( absoluteUri, context ), relativeUri ); ++ } ++ ++ // from no encoded relative url to encoded absolute url ++ { ++ QString relativeUri = "type=mbtiles&url=file:./isle_of_man.mbtiles"; ++ QString absoluteUri = "type=mbtiles&url=" + QString( QUrl::toPercentEncoding( "file://" + QStringLiteral( TEST_DATA_DIR ) + "/isle_of_man.mbtiles" ) ); ++ QCOMPARE( wmsMetadata->relativeToAbsoluteUri( relativeUri, context ), absoluteUri ); ++ } ++ ++ // from encoded to encoded ++ { ++ QString absoluteUri = "type=mbtiles&url=" + QString( QUrl::toPercentEncoding( "file://" + QStringLiteral( TEST_DATA_DIR ) + "/isle_of_man.mbtiles" ) ); ++ QString relativeUri = "type=mbtiles&url=file%3A.%2Fisle_of_man.mbtiles"; ++ QCOMPARE( wmsMetadata->absoluteToRelativeUri( absoluteUri, context ), relativeUri ); ++ QCOMPARE( wmsMetadata->relativeToAbsoluteUri( relativeUri, context ), absoluteUri ); ++ } + } + + void TestQgsWmsProvider::testXyzIsBasemap() +diff --git a/tests/src/python/test_qgsvectortile.py b/tests/src/python/test_qgsvectortile.py +index a4866d1229b..4c42b630b58 100644 +--- a/tests/src/python/test_qgsvectortile.py ++++ b/tests/src/python/test_qgsvectortile.py +@@ -105,7 +105,7 @@ class TestVectorTile(QgisTestCase): + + parts["path"] = "/my/new/file.mbtiles" + uri = md.encodeUri(parts) +- self.assertEqual(uri, "type=mbtiles&url=/my/new/file.mbtiles") ++ self.assertEqual(uri, "type=mbtiles&url=%2Fmy%2Fnew%2Ffile.mbtiles") + + uri = ( + "type=xyz&url=https://fake.server/%7Bx%7D/%7By%7D/%7Bz%7D.png&zmin=0&zmax=2" +@@ -125,7 +125,7 @@ class TestVectorTile(QgisTestCase): + uri = md.encodeUri(parts) + self.assertEqual( + uri, +- "type=xyz&url=https://fake.new.server/%7Bx%7D/%7By%7D/%7Bz%7D.png&zmax=2&zmin=0", ++ "type=xyz&url=https%3A%2F%2Ffake.new.server%2F%7Bx%7D%2F%7By%7D%2F%7Bz%7D.png&zmax=2&zmin=0", + ) + + uri = "type=xyz&serviceType=arcgis&url=https://fake.server/%7Bx%7D/%7By%7D/%7Bz%7D.png&zmax=2&http-header:referer=https://qgis.org/&styleUrl=https://qgis.org/" +@@ -147,7 +147,7 @@ class TestVectorTile(QgisTestCase): + uri = md.encodeUri(parts) + self.assertEqual( + uri, +- "serviceType=arcgis&styleUrl=https://qgis.org/&type=xyz&url=https://fake.new.server/%7Bx%7D/%7By%7D/%7Bz%7D.png&zmax=2&http-header:referer=https://qgis.org/", ++ "serviceType=arcgis&styleUrl=https%3A%2F%2Fqgis.org%2F&type=xyz&url=https%3A%2F%2Ffake.new.server%2F%7Bx%7D%2F%7By%7D%2F%7Bz%7D.png&zmax=2&http-header:referer=https%3A%2F%2Fqgis.org%2F", + ) + + def testZoomRange(self): +diff --git a/tests/src/server/wms/test_qgsserver_wms_parameters.cpp b/tests/src/server/wms/test_qgsserver_wms_parameters.cpp +index 792325c642b..5aa2ab3bd9f 100644 +--- a/tests/src/server/wms/test_qgsserver_wms_parameters.cpp ++++ b/tests/src/server/wms/test_qgsserver_wms_parameters.cpp +@@ -64,14 +64,14 @@ void TestQgsServerWmsParameters::external_layers() + + QgsWms::QgsWmsParametersLayer layer_params = layers_params[0]; + QCOMPARE( layer_params.mNickname, QString( "external_layer_1" ) ); +- QCOMPARE( layer_params.mExternalUri, QString( "layers=layer_1_name&url=http://url_1" ) ); ++ QCOMPARE( layer_params.mExternalUri, QString( "layers=layer_1_name&url=http%3A%2F%2Furl_1" ) ); + + layer_params = layers_params[1]; + QCOMPARE( layer_params.mNickname, QString( "layer" ) ); + + layer_params = layers_params[2]; + QCOMPARE( layer_params.mNickname, QString( "external_layer_2" ) ); +- QCOMPARE( layer_params.mExternalUri, QString( "layers=layer_2_name&opacities=100&url=http://url_2" ) ); ++ QCOMPARE( layer_params.mExternalUri, QString( "layers=layer_2_name&opacities=100&url=http%3A%2F%2Furl_2" ) ); + + //test if opacities are also applied to external layers + QCOMPARE( layers_params[0].mOpacity, 255 ); +@@ -94,7 +94,7 @@ void TestQgsServerWmsParameters::external_layers() + + QgsWms::QgsWmsParametersLayer layer_params2 = layers_params2[0]; + QCOMPARE( layer_params2.mNickname, QString( "external_layer_1" ) ); +- QCOMPARE( layer_params2.mExternalUri, QString( "layers=layer_1_name&url=http://url_1" ) ); ++ QCOMPARE( layer_params2.mExternalUri, QString( "layers=layer_1_name&url=http%3A%2F%2Furl_1" ) ); + } + + void TestQgsServerWmsParameters::percent_encoding() +diff --git a/src/providers/wms/qgswmsprovider.cpp b/src/providers/wms/qgswmsprovider.cpp +index f7a17b8c6ba..429dafa69dd 100644 +--- a/src/providers/wms/qgswmsprovider.cpp ++++ b/src/providers/wms/qgswmsprovider.cpp +@@ -4983,7 +4983,7 @@ QList QgsWmsProviderMetadata::dataItemProviders() const + QVariantMap QgsWmsProviderMetadata::decodeUri( const QString &uri ) const + { + const QUrlQuery query { uri }; +- const QList> constItems { query.queryItems() }; ++ const QList> constItems { query.queryItems( QUrl::ComponentFormattingOption::FullyDecoded ) }; + QVariantMap decoded; + for ( const QPair &item : constItems ) + { +@@ -5035,24 +5035,31 @@ QString QgsWmsProviderMetadata::encodeUri( const QVariantMap &parts ) const + { + if ( it.key() == QLatin1String( "path" ) ) + { +- items.push_back( { QStringLiteral( "url" ), QUrl::fromLocalFile( it.value().toString() ).toString() } ); ++ items.push_back( { QStringLiteral( "url" ), QUrl::toPercentEncoding( QUrl::fromLocalFile( it.value().toString() ).toString() ) } ); + } + else if ( it.key() == QLatin1String( "url" ) ) + { + if ( !parts.contains( QLatin1String( "path" ) ) ) + { +- items.push_back( { it.key(), it.value().toString() } ); ++ items.push_back( { it.key(), QUrl::toPercentEncoding( it.value().toString() ) } ); + } + } + else + { + if ( it.value().userType() == QMetaType::Type::QStringList ) + { +- listItems.push_back( { it.key(), it.value().toStringList() } ); ++ QStringList encodedList; ++ const QStringList unencodedList = it.value().toStringList(); ++ encodedList.reserve( unencodedList.size() ); ++ for ( const QString &item : unencodedList ) ++ { ++ encodedList << QUrl::toPercentEncoding( item ); ++ } ++ listItems.push_back( { it.key(), encodedList } ); + } + else + { +- items.push_back( { it.key(), it.value().toString() } ); ++ items.push_back( { it.key(), QUrl::toPercentEncoding( it.value().toString() ) } ); + } + } + } +diff --git a/tests/src/providers/testqgswmsprovider.cpp b/tests/src/providers/testqgswmsprovider.cpp +index 7f07c933b6d..5ac4d3c43c9 100644 +--- a/tests/src/providers/testqgswmsprovider.cpp ++++ b/tests/src/providers/testqgswmsprovider.cpp +@@ -319,7 +319,7 @@ void TestQgsWmsProvider::testMbtilesProviderMetadata() + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "wms" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "isle_of_man" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "url=file://%1/isle_of_man.mbtiles&type=mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), u"url=file%3A%2F%2F%1%2Fisle_of_man.mbtiles&type=mbtiles"_s.arg( QString( TEST_DATA_DIR ).replace( "/", "%2F" ) ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::Raster ); + QVERIFY( !sublayers.at( 0 ).skippedContainerScan() ); + QVERIFY( !QgsProviderUtils::sublayerDetailsAreIncomplete( sublayers ) ); +@@ -328,7 +328,7 @@ void TestQgsWmsProvider::testMbtilesProviderMetadata() + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "wms" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "isle_of_man" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "url=file://%1/isle_of_man.mbtiles&type=mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), u"url=file%3A%2F%2F%1%2Fisle_of_man.mbtiles&type=mbtiles"_s.arg( QString( TEST_DATA_DIR ).replace( "/", "%2F" ) ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::Raster ); + QVERIFY( !sublayers.at( 0 ).skippedContainerScan() ); + +@@ -345,16 +345,16 @@ void TestQgsWmsProvider::testMbtilesProviderMetadata() + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "wms" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "isle_of_man" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "url=file://%1/isle_of_man.mbtiles&type=mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), u"url=file%3A%2F%2F%1%2Fisle_of_man.mbtiles&type=mbtiles"_s.arg( QString( TEST_DATA_DIR ).replace( "/", "%2F" ) ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::Raster ); + QVERIFY( sublayers.at( 0 ).skippedContainerScan() ); + QVERIFY( QgsProviderUtils::sublayerDetailsAreIncomplete( sublayers ) ); + +- sublayers = wmsMetadata->querySublayers( QStringLiteral( "type=mbtiles&url=%1/isle_of_man.mbtiles" ).arg( TEST_DATA_DIR ), Qgis::SublayerQueryFlag::FastScan ); ++ sublayers = wmsMetadata->querySublayers( u"type=mbtiles&url=file%3A%2F%2F%1%2Fisle_of_man.mbtiles"_s.arg( TEST_DATA_DIR ), Qgis::SublayerQueryFlag::FastScan ); + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "wms" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "isle_of_man" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "url=file://%1/isle_of_man.mbtiles&type=mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), u"url=file%3A%2F%2F%1%2Fisle_of_man.mbtiles&type=mbtiles"_s.arg( QString( TEST_DATA_DIR ).replace( "/", "%2F" ) ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::Raster ); + QVERIFY( sublayers.at( 0 ).skippedContainerScan() ); + +@@ -372,7 +372,7 @@ void TestQgsWmsProvider::testMbtilesProviderMetadata() + QCOMPARE( sublayers.size(), 1 ); + QCOMPARE( sublayers.at( 0 ).providerKey(), QStringLiteral( "wms" ) ); + QCOMPARE( sublayers.at( 0 ).name(), QStringLiteral( "mbtiles_vt" ) ); +- QCOMPARE( sublayers.at( 0 ).uri(), QStringLiteral( "url=file://%1/vector_tile/mbtiles_vt.mbtiles&type=mbtiles" ).arg( TEST_DATA_DIR ) ); ++ QCOMPARE( sublayers.at( 0 ).uri(), u"url=file%3A%2F%2F%1%2Fvector_tile%2Fmbtiles_vt.mbtiles&type=mbtiles"_s.arg( QString( TEST_DATA_DIR ).replace( "/", "%2F" ) ) ); + QCOMPARE( sublayers.at( 0 ).type(), Qgis::LayerType::Raster ); + QVERIFY( sublayers.at( 0 ).skippedContainerScan() ); + +@@ -433,22 +433,21 @@ void TestQgsWmsProvider::providerUriUpdates() + QCOMPARE( parts["testParam"], QVariant( "false" ) ); + + QString updatedUri = metadata->encodeUri( parts ); +- QString expectedUri = QStringLiteral( "crs=EPSG:4326&dpiMode=7&" ++ QString expectedUri = QStringLiteral( "crs=EPSG%3A4326&dpiMode=7&" + "layers=testlayer&styles&" + "testParam=false&" +- "url=http://localhost:8380/mapserv" ); ++ "url=http%3A%2F%2Flocalhost%3A8380%2Fmapserv" ); + QCOMPARE( updatedUri, expectedUri ); + } + + void TestQgsWmsProvider::providerUriLocalFile() + { +- QString uriString = QStringLiteral( "url=file:///my/local/tiles.mbtiles&type=mbtiles" ); +- QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "wms" ), uriString ); ++ QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( u"wms"_s, u"url=file:///my/local/tiles.mbtiles&type=mbtiles"_s ); + QVariantMap expectedParts { { QString( "type" ), QVariant( "mbtiles" ) }, { QString( "path" ), QVariant( "/my/local/tiles.mbtiles" ) }, { QString( "url" ), QVariant( "file:///my/local/tiles.mbtiles" ) } }; + QCOMPARE( parts, expectedParts ); + + QString encodedUri = QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "wms" ), parts ); +- QCOMPARE( encodedUri, uriString ); ++ QCOMPARE( encodedUri, u"url=file%3A%2F%2F%2Fmy%2Flocal%2Ftiles.mbtiles&type=mbtiles"_s ); + + QgsProviderMetadata *wmsMetadata = QgsProviderRegistry::instance()->providerMetadata( "wms" ); + QVERIFY( wmsMetadata ); +diff --git a/tests/src/python/test_qgsmapboxglconverter.py b/tests/src/python/test_qgsmapboxglconverter.py +index 16c0b6652a5..2da4bce4e0c 100644 +--- a/tests/src/python/test_qgsmapboxglconverter.py ++++ b/tests/src/python/test_qgsmapboxglconverter.py +@@ -2554,7 +2554,7 @@ class TestQgsMapBoxGlStyleConverter(QgisTestCase): + self.assertIsInstance(rl, QgsRasterLayer) + self.assertEqual( + rl.source(), +- "tilePixelRation=1&type=xyz&url=https://yyyyyy/v1/tiles/texturereliefshade/EPSG:3857/%7Bz%7D/%7Bx%7D/%7By%7D.webp&zmax=20&zmin=3", ++ "tilePixelRation=1&type=xyz&url=https%3A%2F%2Fyyyyyy%2Fv1%2Ftiles%2Ftexturereliefshade%2FEPSG%3A3857%2F%7Bz%7D%2F%7Bx%7D%2F%7By%7D.webp&zmax=20&zmin=3", + ) + self.assertEqual(rl.providerType(), "wms") + +@@ -2566,7 +2566,7 @@ class TestQgsMapBoxGlStyleConverter(QgisTestCase): + self.assertEqual(raster_layer.name(), "Texture-Relief") + self.assertEqual( + raster_layer.source(), +- "tilePixelRation=1&type=xyz&url=https://yyyyyy/v1/tiles/texturereliefshade/EPSG:3857/%7Bz%7D/%7Bx%7D/%7By%7D.webp&zmax=20&zmin=3", ++ "tilePixelRation=1&type=xyz&url=https%3A%2F%2Fyyyyyy%2Fv1%2Ftiles%2Ftexturereliefshade%2FEPSG%3A3857%2F%7Bz%7D%2F%7Bx%7D%2F%7By%7D.webp&zmax=20&zmin=3", + ) + self.assertEqual( + raster_layer.pipe()