19 #include <QDesktopServices>
24 #include <QTemporaryDir>
25 #include <QNetworkRequest>
26 #include <QJsonDocument>
27 #include <QHttpMultiPart>
28 #include <QMimeDatabase>
29 #include <QApplication>
51 #elif defined(Q_OS_MAC)
69 void QgsAction::handleFormSubmitAction(
const QString &expandedAction )
const
73 QApplication::setOverrideCursor( Qt::WaitCursor );
75 QUrl url{ expandedAction };
78 const QString payload { url.query( QUrl::ComponentFormattingOption::FullyEncoded ).replace( QChar(
'+' ), QStringLiteral(
"%2B" ) ) };
81 const QUrlQuery queryString { url.query( ) };
82 url.setQuery( QString( ) );
84 QNetworkRequest req { url };
88 if ( url.toString().contains( QLatin1String(
"fake_qgis_http_endpoint" ) ) )
90 req.setUrl( QStringLiteral(
"file://%1" ).arg( url.path() ) );
93 QNetworkReply *reply =
nullptr;
97 QString contentType { QStringLiteral(
"application/x-www-form-urlencoded" ) };
99 QJsonParseError jsonError;
100 QJsonDocument::fromJson( payload.toUtf8(), &jsonError );
101 if ( jsonError.error == QJsonParseError::ParseError::NoError )
103 contentType = QStringLiteral(
"application/json" );
105 req.setHeader( QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType );
111 QHttpMultiPart *multiPart =
new QHttpMultiPart( QHttpMultiPart::FormDataType );
112 const QList<QPair<QString, QString>> queryItems { queryString.queryItems( QUrl::ComponentFormattingOption::FullyDecoded ) };
113 for (
const QPair<QString, QString> &queryItem : std::as_const( queryItems ) )
116 part.setHeader( QNetworkRequest::ContentDispositionHeader,
117 QStringLiteral(
"form-data; name=\"%1\"" )
118 .arg( QString( queryItem.first ).replace(
'"', QLatin1String( R
"(\")" ) ) ) );
119 part.setBody( queryItem.second.toUtf8() );
120 multiPart->append( part );
123 multiPart->setParent( reply );
126 QObject::connect( reply, &QNetworkReply::finished, reply, [ reply ]
128 if ( reply->error() == QNetworkReply::NoError )
131 if ( reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).isNull() )
134 const QByteArray replyData = reply->readAll();
136 QString filename {
"download.bin" };
137 if ( const std::string header = reply->header( QNetworkRequest::KnownHeaders::ContentDispositionHeader ).toString().toStdString(); ! header.empty() )
141 const std::string q1 { R
"(filename=")" };
142 if ( const unsigned long pos = header.find( q1 ); pos != std::string::npos )
144 const unsigned long len = pos + q1.size();
146 const std::string q2 { R"(")" };
147 if ( unsigned long pos = header.find( q2, len ); pos != std::string::npos )
149 bool escaped = false;
150 while ( pos != std::string::npos && header[pos - 1] == '\\' )
152 pos = header.find( q2, pos + 1 );
155 ascii = header.substr( len, pos - len );
159 for ( size_t i = 0; i < ascii.size(); ++i )
161 if ( ascii[i] ==
'\\' )
163 if ( i > 0 && ascii[i - 1] ==
'\\' )
165 cleaned.push_back( ascii[i] );
170 cleaned.push_back( ascii[i] );
180 const std::string u { R
"(UTF-8'')" };
181 if ( const unsigned long pos = header.find( u ); pos != std::string::npos )
183 utf8 = header.substr( pos + u.size() );
189 if ( ! utf8.empty( ) )
191 filename = QString::fromStdString( utf8 );
196 filename = QString::fromStdString( ascii );
199 else if ( !reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).isNull() )
201 QString contentTypeHeader { reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).toString() };
203 if ( contentTypeHeader.contains(
';' ) )
205 contentTypeHeader = contentTypeHeader.left( contentTypeHeader.indexOf(
';' ) );
208 QMimeType mimeType { QMimeDatabase().mimeTypeForName( contentTypeHeader ) };
209 if ( mimeType.isValid() )
211 filename = QStringLiteral(
"download.%1" ).arg( mimeType.preferredSuffix() );
215 QTemporaryDir tempDir;
216 tempDir.setAutoRemove( false );
218 const QString tempFilePath{ tempDir.path() + QDir::separator() + filename };
219 QFile tempFile{ tempFilePath };
220 tempFile.open( QIODevice::WriteOnly );
221 tempFile.write( replyData );
223 QDesktopServices::openUrl( QUrl::fromLocalFile( tempFilePath ) );
227 QgsMessageLog::logMessage( QObject::tr(
"Redirect is not supported!" ), QStringLiteral(
"Form Submit Action" ), Qgis::MessageLevel::Critical );
234 reply->deleteLater();
235 QApplication::restoreOverrideCursor( );
242 mCommand = newCommand;
249 QgsDebugMsg( QStringLiteral(
"Invalid action cannot be run" ) );
258 QApplication::setOverrideCursor( Qt::WaitCursor );
260 QApplication::restoreOverrideCursor();
264 const QFileInfo finfo( expandedAction );
265 if ( finfo.exists() && finfo.isFile() )
266 QDesktopServices::openUrl( QUrl::fromLocalFile( expandedAction ) );
268 QDesktopServices::openUrl( QUrl( expandedAction, QUrl::TolerantMode ) );
272 handleFormSubmitAction( expandedAction );
289 return mActionScopes;
299 QDomElement actionElement = actionNode.toElement();
300 const QDomNodeList actionScopeNodes = actionElement.elementsByTagName( QStringLiteral(
"actionScope" ) );
302 if ( actionScopeNodes.isEmpty() )
305 << QStringLiteral(
"Canvas" )
306 << QStringLiteral(
"Field" )
307 << QStringLiteral(
"Feature" );
311 for (
int j = 0; j < actionScopeNodes.length(); ++j )
313 const QDomElement actionScopeElem = actionScopeNodes.item( j ).toElement();
314 mActionScopes << actionScopeElem.attribute( QStringLiteral(
"id" ) );
318 mType =
static_cast< QgsAction::ActionType >( actionElement.attributeNode( QStringLiteral(
"type" ) ).value().toInt() );
319 mDescription = actionElement.attributeNode( QStringLiteral(
"name" ) ).value();
320 mCommand = actionElement.attributeNode( QStringLiteral(
"action" ) ).value();
321 mIcon = actionElement.attributeNode( QStringLiteral(
"icon" ) ).value();
322 mCaptureOutput = actionElement.attributeNode( QStringLiteral(
"capture" ) ).value().toInt() != 0;
323 mShortTitle = actionElement.attributeNode( QStringLiteral(
"shortTitle" ) ).value();
324 mNotificationMessage = actionElement.attributeNode( QStringLiteral(
"notificationMessage" ) ).value();
325 mIsEnabledOnlyWhenEditable = actionElement.attributeNode( QStringLiteral(
"isEnabledOnlyWhenEditable" ) ).value().toInt() != 0;
326 mId = QUuid( actionElement.attributeNode( QStringLiteral(
"id" ) ).value() );
328 mId = QUuid::createUuid();
333 QDomElement actionSetting = actionsNode.ownerDocument().createElement( QStringLiteral(
"actionsetting" ) );
334 actionSetting.setAttribute( QStringLiteral(
"type" ), mType );
335 actionSetting.setAttribute( QStringLiteral(
"name" ), mDescription );
336 actionSetting.setAttribute( QStringLiteral(
"shortTitle" ), mShortTitle );
337 actionSetting.setAttribute( QStringLiteral(
"icon" ), mIcon );
338 actionSetting.setAttribute( QStringLiteral(
"action" ), mCommand );
339 actionSetting.setAttribute( QStringLiteral(
"capture" ), mCaptureOutput );
340 actionSetting.setAttribute( QStringLiteral(
"notificationMessage" ), mNotificationMessage );
341 actionSetting.setAttribute( QStringLiteral(
"isEnabledOnlyWhenEditable" ), mIsEnabledOnlyWhenEditable );
342 actionSetting.setAttribute( QStringLiteral(
"id" ), mId.toString() );
344 const auto constMActionScopes = mActionScopes;
345 for (
const QString &scope : constMActionScopes )
347 QDomElement actionScopeElem = actionsNode.ownerDocument().createElement( QStringLiteral(
"actionScope" ) );
348 actionScopeElem.setAttribute( QStringLiteral(
"id" ), scope );
349 actionSetting.appendChild( actionScopeElem );
352 actionsNode.appendChild( actionSetting );
357 mExpressionContextScope = scope;
362 return mExpressionContextScope;
372 typeText = QObject::tr(
"Generic" );
377 typeText = QObject::tr(
"Generic Python" );
382 typeText = QObject::tr(
"Mac" );
387 typeText = QObject::tr(
"Windows" );
392 typeText = QObject::tr(
"Unix" );
397 typeText = QObject::tr(
"Open URL" );
402 typeText = QObject::tr(
"Submit URL (urlencoded or JSON)" );
407 typeText = QObject::tr(
"Submit URL (multipart)" );
411 return { QObject::tr( R
"html(
412 <h2>Action Details</h2>
414 <b>Description:</b> %1<br>
415 <b>Short title:</b> %2<br>
421 )html" ).arg( mDescription, mShortTitle, typeText, actionScopes().values().join( QLatin1String( ", " ) ), mCommand )};