From a90e34a765d061f338b2564accb6a6b140863e9c Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 7 Dec 2021 09:33:04 +0100 Subject: [PATCH] Address PR comments --- resources/function_help/json/url_encode | 2 +- src/core/qgsaction.cpp | 341 ++++++++++++------------ src/core/qgsaction.h | 2 + 3 files changed, 171 insertions(+), 174 deletions(-) diff --git a/resources/function_help/json/url_encode b/resources/function_help/json/url_encode index 91d93f25af0..0cd91eab1aa 100644 --- a/resources/function_help/json/url_encode +++ b/resources/function_help/json/url_encode @@ -2,7 +2,7 @@ "name": "url_encode", "type": "function", "groups": ["Maps"], - "description": "Returns an URL encoded string from a map.
The underlying function uses QUrlQuery::toString with QUrl::FullyEncoded flags.
Note that the plus sign '+' is not converted.", + "description": "Returns an URL encoded string from a map. Transforms all characters in their properly-encoded form producing a fully-compliant query string.
Note that the plus sign '+' is not converted.", "arguments": [ { "arg": "map", diff --git a/src/core/qgsaction.cpp b/src/core/qgsaction.cpp index 45df0618b36..60783ea2498 100644 --- a/src/core/qgsaction.cpp +++ b/src/core/qgsaction.cpp @@ -65,6 +65,173 @@ void QgsAction::run( QgsVectorLayer *layer, const QgsFeature &feature, const Qgs run( actionContext ); } +void QgsAction::handleFormSubmitAction( const QString &expandedAction ) const +{ + + QUrl url{ expandedAction }; + + // Encode '+' (fully encoded doesn't encode it) + const QString payload { url.query( QUrl::ComponentFormattingOption::FullyEncoded ).replace( QChar( '+' ), QStringLiteral( "%2B" ) ) }; + + // Remove query string from URL + const QUrlQuery queryString { url.query( ) }; + url.setQuery( QString( ) ); + + QNetworkRequest req { url }; + + // Specific code for testing, produces an invalid POST but we can still listen to + // signals and examine the request + if ( url.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) ) + { + req.setUrl( QStringLiteral( "file://%1" ).arg( url.path() ) ); + } + + QNetworkReply *reply = nullptr; + + if ( mType != QgsAction::SubmitUrlMultipart ) + { + QString contentType { QStringLiteral( "application/x-www-form-urlencoded" ) }; + // check for json + QJsonParseError jsonError; + QJsonDocument::fromJson( payload.toUtf8(), &jsonError ); + if ( jsonError.error == QJsonParseError::ParseError::NoError ) + { + contentType = QStringLiteral( "application/json" ); + } + req.setHeader( QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType ); + reply = QgsNetworkAccessManager::instance()->post( req, payload.toUtf8() ); + } + // for multipart create parts and headers + else + { + QHttpMultiPart *multiPart = new QHttpMultiPart( QHttpMultiPart::FormDataType ); + const QList> queryItems { queryString.queryItems( QUrl::ComponentFormattingOption::FullyDecoded ) }; + for ( const QPair &queryItem : std::as_const( queryItems ) ) + { + QHttpPart part; + part.setHeader( QNetworkRequest::ContentDispositionHeader, + QStringLiteral( "form-data; name=\"%1\"" ) + .arg( QString( queryItem.first ).replace( '"', QStringLiteral( R"(\")" ) ) ) ); + part.setBody( queryItem.second.toUtf8() ); + multiPart->append( part ); + } + reply = QgsNetworkAccessManager::instance()->post( req, multiPart ); + multiPart->setParent( reply ); + } + + QObject::connect( reply, &QNetworkReply::finished, reply, [ reply ] + { + if ( reply->error() == QNetworkReply::NoError ) + { + + if ( reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).isNull() ) + { + + const QByteArray replyData = reply->readAll(); + + QString filename { "download.bin" }; + if ( const std::string header = reply->header( QNetworkRequest::KnownHeaders::ContentDispositionHeader ).toString().toStdString(); ! header.empty() ) + { + + std::string ascii; + const std::string q1 { R"(filename=")" }; + if ( const unsigned long pos = header.find( q1 ); pos != std::string::npos ) + { + const unsigned long len = pos + q1.size(); + + const std::string q2 { R"(")" }; + if ( unsigned long pos = header.find( q2, len ); pos != std::string::npos ) + { + bool escaped = false; + while ( pos != std::string::npos && header[pos - 1] == '\\' ) + { + pos = header.find( q2, pos + 1 ); + escaped = true; + } + ascii = header.substr( len, pos - len ); + if ( escaped ) + { + std::string cleaned; + for ( size_t i = 0; i < ascii.size(); ++i ) + { + if ( ascii[i] == '\\' ) + { + if ( i > 0 && ascii[i - 1] == '\\' ) + { + cleaned.push_back( ascii[i] ); + } + } + else + { + cleaned.push_back( ascii[i] ); + } + } + ascii = cleaned; + } + } + } + + std::string utf8; + + const std::string u { R"(UTF-8'')" }; + if ( const unsigned long pos = header.find( u ); pos != std::string::npos ) + { + utf8 = header.substr( pos + u.size() ); + } + + // Prefer ascii over utf8 + if ( ascii.empty() ) + { + if ( ! utf8.empty( ) ) + { + filename = QString::fromStdString( utf8 ); + } + } + else + { + filename = QString::fromStdString( ascii ); + } + } + else if ( !reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).isNull() ) + { + QString contentTypeHeader { reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).toString() }; + // Strip charset if any + if ( contentTypeHeader.contains( ';' ) ) + { + contentTypeHeader = contentTypeHeader.left( contentTypeHeader.indexOf( ';' ) ); + } + + QMimeType mimeType { QMimeDatabase().mimeTypeForName( contentTypeHeader ) }; + if ( mimeType.isValid() ) + { + filename = QStringLiteral( "download.%1" ).arg( mimeType.preferredSuffix() ); + } + } + + QTemporaryDir tempDir; + tempDir.setAutoRemove( false ); + tempDir.path(); + const QString tempFilePath{ tempDir.path() + QDir::separator() + filename }; + QFile tempFile{ tempFilePath }; + tempFile.open( QIODevice::WriteOnly ); + tempFile.write( replyData ); + tempFile.close(); + QDesktopServices::openUrl( QUrl::fromLocalFile( tempFilePath ) ); + } + else + { + QgsMessageLog::logMessage( QObject::tr( "Redirect is not supported!" ), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical ); + } + } + else + { + QgsMessageLog::logMessage( reply->errorString(), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical ); + } + reply->deleteLater(); + } ); + +} + void QgsAction::run( const QgsExpressionContext &expressionContext ) const { if ( !isValid() ) @@ -89,179 +256,7 @@ void QgsAction::run( const QgsExpressionContext &expressionContext ) const } else if ( mType == QgsAction::SubmitUrlEncoded || mType == QgsAction::SubmitUrlMultipart ) { - - QUrl url{ expandedAction }; - - // Encode '+' (fully encoded doesn't encode it) - const QString payload { url.query( QUrl::ComponentFormattingOption::FullyEncoded ).replace( QChar( '+' ), QStringLiteral( "%2B" ) ) }; - - // Remove query string from URL - const QUrlQuery queryString { url.query( ) }; - url.setQuery( QString( ) ); - - QNetworkRequest req { url }; - - // Specific code for testing, produces an invalid POST but we can still listen to - // signals and examine the request - if ( url.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) ) - { - req.setUrl( QStringLiteral( "file://%1" ).arg( url.path() ) ); - } - - QNetworkReply *reply = nullptr; - - if ( mType != QgsAction::SubmitUrlMultipart ) - { - QString contentType { QStringLiteral( "application/x-www-form-urlencoded" ) }; - // check for json - QJsonParseError jsonError; - QJsonDocument::fromJson( payload.toUtf8(), &jsonError ); - if ( jsonError.error == QJsonParseError::ParseError::NoError ) - { - contentType = QStringLiteral( "application/json" ); - } - req.setHeader( QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType ); - reply = QgsNetworkAccessManager::instance()->post( req, payload.toUtf8() ); - } - // for multipart create parts and headers - else - { - QHttpMultiPart *multiPart = new QHttpMultiPart( QHttpMultiPart::FormDataType ); - const auto queryItems { queryString.queryItems( QUrl::ComponentFormattingOption::FullyDecoded ) }; - for ( const auto &queryItem : std::as_const( queryItems ) ) - { - QHttpPart part; - part.setHeader( QNetworkRequest::ContentDispositionHeader, - QStringLiteral( "form-data; name=\"%1\"" ) - .arg( QString( queryItem.first ).replace( '"', QStringLiteral( R"(\")" ) ) ) ); - part.setBody( queryItem.second.toUtf8() ); - multiPart->append( part ); - } - reply = QgsNetworkAccessManager::instance()->post( req, multiPart ); - multiPart->setParent( reply ); - } - - QObject::connect( reply, &QNetworkReply::finished, reply, [ reply ] - { - if ( reply->error() == QNetworkReply::NoError ) - { - - if ( reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).isNull() ) - { - - const QByteArray replyData = reply->readAll(); - - QString filename { "download.bin" }; - if ( const auto header = reply->header( QNetworkRequest::KnownHeaders::ContentDispositionHeader ).toString().toStdString(); ! header.empty() ) - { - - std::string ascii; - const std::string q1 { R"(filename=")" }; - if ( const auto pos = header.find( q1 ); pos != std::string::npos ) - { - const auto len = pos + q1.size(); - - const std::string q2 { R"(")" }; - if ( auto pos = header.find( q2, len ); pos != std::string::npos ) - { - bool escaped = false; - while ( pos != std::string::npos && header[pos - 1] == '\\' ) - { - pos = header.find( q2, pos + 1 ); - escaped = true; - } - ascii = header.substr( len, pos - len ); - if ( escaped ) - { - std::string cleaned; - for ( size_t i = 0; i < ascii.size(); ++i ) - { - if ( ascii[i] == '\\' ) - { - if ( i > 0 && ascii[i - 1] == '\\' ) - { - cleaned.push_back( ascii[i] ); - } - } - else - { - cleaned.push_back( ascii[i] ); - } - } - ascii = cleaned; - } - } - } - - std::string utf8; - - const std::string u { R"(UTF-8'')" }; - if ( const auto pos = header.find( u ); pos != std::string::npos ) - { - utf8 = header.substr( pos + u.size() ); - } - - // Prefer ascii over utf8 - if ( ascii.empty() ) - { - if ( ! utf8.empty( ) ) - { - filename = QString::fromStdString( utf8 ); - } - } - else - { - filename = QString::fromStdString( ascii ); - } - } - else if ( !reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).isNull() ) - { - QString contentTypeHeader { reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).toString() }; - // Strip charset if any - if ( contentTypeHeader.contains( ';' ) ) - { - contentTypeHeader = contentTypeHeader.left( contentTypeHeader.indexOf( ';' ) ); - } - - QMimeType mimeType { QMimeDatabase().mimeTypeForName( contentTypeHeader ) }; - if ( mimeType.isValid() ) - { - filename = QStringLiteral( "download.%1" ).arg( mimeType.preferredSuffix() ); - } - } - - QTemporaryDir tempDir; - tempDir.setAutoRemove( false ); - tempDir.path(); - const QString tempFilePath{ tempDir.path() + QDir::separator() + filename }; - QFile tempFile{ tempFilePath }; - tempFile.open( QIODevice::WriteOnly ); - tempFile.write( replyData ); - tempFile.close(); - QDesktopServices::openUrl( QUrl::fromLocalFile( tempFilePath ) ); - } - else - { - QgsMessageLog::logMessage( QObject::tr( "Redirect is not supported!" ), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical ); - } - } - else - { - QgsMessageLog::logMessage( reply->errorString(), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical ); - } - reply->deleteLater(); - } ); - -#if 0 // Not sure we need this: error is checked in finished() anyway -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - QObject::connect( reply, qOverload( QNetworkReply::error error ), reply, [ reply ] {} ); -#else - QObject::connect( reply, &QNetworkReply::errorOccurred, reply, [ reply ]( QNetworkReply::NetworkError ) - { - QgsMessageLog::logMessage( reply->errorString(), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical ); - } ); -#endif -#endif + handleFormSubmitAction( expandedAction ); } else if ( mType == QgsAction::GenericPython ) diff --git a/src/core/qgsaction.h b/src/core/qgsaction.h index 075fcf0fc90..e99fadbad5c 100644 --- a/src/core/qgsaction.h +++ b/src/core/qgsaction.h @@ -257,6 +257,8 @@ class CORE_EXPORT QgsAction QString html( ) const; private: + + void handleFormSubmitAction( const QString &expandedAction ) const; ActionType mType = Generic; QString mDescription; QString mShortTitle;