QGIS API Documentation 3.29.0-Master (53715c36dc)
qgsaction.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsaction.cpp - QgsAction
3
4 ---------------------
5 begin : 18.4.2016
6 copyright : (C) 2016 by Matthias Kuhn
8 ***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
17#include "qgsaction.h"
18
19#include <QDesktopServices>
20#include <QFileInfo>
21#include <QUrl>
22#include <QUrlQuery>
23#include <QDir>
24#include <QTemporaryDir>
25#include <QNetworkRequest>
26#include <QJsonDocument>
27#include <QHttpMultiPart>
28#include <QMimeDatabase>
29#include <QApplication>
30
31#include "qgspythonrunner.h"
32#include "qgsrunprocess.h"
34#include "qgsvectorlayer.h"
35#include "qgslogger.h"
37#include "qgswebview.h"
39#include "qgsmessagelog.h"
40#include "qgsvariantutils.h"
41
43{
44 return mType == Generic ||
45 mType == GenericPython ||
46 mType == OpenUrl ||
47 mType == SubmitUrlEncoded ||
48 mType == SubmitUrlMultipart ||
49#if defined(Q_OS_WIN)
50 mType == Windows
51#elif defined(Q_OS_MAC)
52 mType == Mac
53#else
54 mType == Unix
55#endif
56 ;
57}
58
59void QgsAction::run( QgsVectorLayer *layer, const QgsFeature &feature, const QgsExpressionContext &expressionContext ) const
60{
61 QgsExpressionContext actionContext( expressionContext );
62
63 actionContext << QgsExpressionContextUtils::layerScope( layer );
64 actionContext.setFeature( feature );
65
66 run( actionContext );
67}
68
69void QgsAction::handleFormSubmitAction( const QString &expandedAction ) const
70{
71
72 // Show busy in case the form subit is slow
73 QApplication::setOverrideCursor( Qt::WaitCursor );
74
75 QUrl url{ expandedAction };
76
77 // Encode '+' (fully encoded doesn't encode it)
78 const QString payload { url.query( QUrl::ComponentFormattingOption::FullyEncoded ).replace( QChar( '+' ), QStringLiteral( "%2B" ) ) };
79
80 // Remove query string from URL
81 const QUrlQuery queryString { url.query( ) };
82 url.setQuery( QString( ) );
83
84 QNetworkRequest req { url };
85
86 // Specific code for testing, produces an invalid POST but we can still listen to
87 // signals and examine the request
88 if ( url.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
89 {
90 req.setUrl( QStringLiteral( "file://%1" ).arg( url.path() ) );
91 }
92
93 QNetworkReply *reply = nullptr;
94
95 if ( mType != QgsAction::SubmitUrlMultipart )
96 {
97 QString contentType { QStringLiteral( "application/x-www-form-urlencoded" ) };
98 // check for json
99 QJsonParseError jsonError;
100 QJsonDocument::fromJson( payload.toUtf8(), &jsonError );
101 if ( jsonError.error == QJsonParseError::ParseError::NoError )
102 {
103 contentType = QStringLiteral( "application/json" );
104 }
105 req.setHeader( QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType );
106 reply = QgsNetworkAccessManager::instance()->post( req, payload.toUtf8() );
107 }
108 // for multipart create parts and headers
109 else
110 {
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 ) )
114 {
115 QHttpPart part;
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 );
121 }
122 reply = QgsNetworkAccessManager::instance()->post( req, multiPart );
123 multiPart->setParent( reply );
124 }
125
126 QObject::connect( reply, &QNetworkReply::finished, reply, [ reply ]
127 {
128 if ( reply->error() == QNetworkReply::NoError )
129 {
130
131 if ( QgsVariantUtils::isNull( reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) ) )
132 {
133
134 const QByteArray replyData = reply->readAll();
135
136 QString filename { "download.bin" };
137 if ( const std::string header = reply->header( QNetworkRequest::KnownHeaders::ContentDispositionHeader ).toString().toStdString(); ! header.empty() )
138 {
139
140 // Extract filename dealing with ill formed headers with unquoted file names
141
142 std::string ascii;
143
144 const std::string q1 { R"(filename=)" };
145
146 if ( size_t pos = header.find( q1 ); pos != std::string::npos )
147 {
148
149 // Deal with ill formed headers with unquoted file names
150 if ( header.find( R"(filename=")" ) != std::string::npos )
151 {
152 pos++;
153 }
154
155 const size_t len = pos + q1.size();
156
157 const std::string q2 { R"(")" };
158 if ( size_t pos = header.find( q2, len ); pos != std::string::npos )
159 {
160 bool escaped = false;
161 while ( pos != std::string::npos && header[pos - 1] == '\\' )
162 {
163 pos = header.find( q2, pos + 1 );
164 escaped = true;
165 }
166 ascii = header.substr( len, pos - len );
167 if ( escaped )
168 {
169 std::string cleaned;
170 for ( size_t i = 0; i < ascii.size(); ++i )
171 {
172 if ( ascii[i] == '\\' )
173 {
174 if ( i > 0 && ascii[i - 1] == '\\' )
175 {
176 cleaned.push_back( ascii[i] );
177 }
178 }
179 else
180 {
181 cleaned.push_back( ascii[i] );
182 }
183 }
184 ascii = cleaned;
185 }
186 }
187 }
188
189 std::string utf8;
190
191 const std::string u { R"(UTF-8'')" };
192 if ( const size_t pos = header.find( u ); pos != std::string::npos )
193 {
194 utf8 = header.substr( pos + u.size() );
195 }
196
197 // Prefer ascii over utf8
198 if ( ascii.empty() )
199 {
200 if ( ! utf8.empty( ) )
201 {
202 filename = QString::fromStdString( utf8 );
203 }
204 }
205 else
206 {
207 filename = QString::fromStdString( ascii );
208 }
209 }
210 else if ( !QgsVariantUtils::isNull( reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ) ) )
211 {
212 QString contentTypeHeader { reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).toString() };
213 // Strip charset if any
214 if ( contentTypeHeader.contains( ';' ) )
215 {
216 contentTypeHeader = contentTypeHeader.left( contentTypeHeader.indexOf( ';' ) );
217 }
218
219 QMimeType mimeType { QMimeDatabase().mimeTypeForName( contentTypeHeader ) };
220 if ( mimeType.isValid() )
221 {
222 filename = QStringLiteral( "download.%1" ).arg( mimeType.preferredSuffix() );
223 }
224 }
225
226 QTemporaryDir tempDir;
227 tempDir.setAutoRemove( false );
228 tempDir.path();
229 const QString tempFilePath{ tempDir.path() + QDir::separator() + filename };
230 QFile tempFile{ tempFilePath };
231 tempFile.open( QIODevice::WriteOnly );
232 tempFile.write( replyData );
233 tempFile.close();
234 QDesktopServices::openUrl( QUrl::fromLocalFile( tempFilePath ) );
235 }
236 else
237 {
238 QgsMessageLog::logMessage( QObject::tr( "Redirect is not supported!" ), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical );
239 }
240 }
241 else
242 {
243 QgsMessageLog::logMessage( reply->errorString(), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical );
244 }
245 reply->deleteLater();
246 QApplication::restoreOverrideCursor( );
247 } );
248
249}
250
251void QgsAction::setCommand( const QString &newCommand )
252{
253 mCommand = newCommand;
254}
255
256void QgsAction::run( const QgsExpressionContext &expressionContext ) const
257{
258 if ( !isValid() )
259 {
260 QgsDebugMsg( QStringLiteral( "Invalid action cannot be run" ) );
261 return;
262 }
263
264 QgsExpressionContextScope *scope = new QgsExpressionContextScope( mExpressionContextScope );
265 QgsExpressionContext context( expressionContext );
266 context << scope;
267
268 // Show busy in case the expression evaluation is slow
269 QApplication::setOverrideCursor( Qt::WaitCursor );
270 const QString expandedAction = QgsExpression::replaceExpressionText( mCommand, &context );
271 QApplication::restoreOverrideCursor();
272
273 if ( mType == QgsAction::OpenUrl )
274 {
275 const QFileInfo finfo( expandedAction );
276 if ( finfo.exists() && finfo.isFile() )
277 QDesktopServices::openUrl( QUrl::fromLocalFile( expandedAction ) );
278 else
279 QDesktopServices::openUrl( QUrl( expandedAction, QUrl::TolerantMode ) );
280 }
281 else if ( mType == QgsAction::SubmitUrlEncoded || mType == QgsAction::SubmitUrlMultipart )
282 {
283 handleFormSubmitAction( expandedAction );
284 }
285 else if ( mType == QgsAction::GenericPython )
286 {
287 // TODO: capture output from QgsPythonRunner (like QgsRunProcess does)
288 QgsPythonRunner::run( expandedAction );
289 }
290 else
291 {
292 // The QgsRunProcess instance created by this static function
293 // deletes itself when no longer needed.
294 QgsRunProcess::create( expandedAction, mCaptureOutput );
295 }
296}
297
298QSet<QString> QgsAction::actionScopes() const
299{
300 return mActionScopes;
301}
302
303void QgsAction::setActionScopes( const QSet<QString> &actionScopes )
304{
305 mActionScopes = actionScopes;
306}
307
308void QgsAction::readXml( const QDomNode &actionNode )
309{
310 QDomElement actionElement = actionNode.toElement();
311 const QDomNodeList actionScopeNodes = actionElement.elementsByTagName( QStringLiteral( "actionScope" ) );
312
313 if ( actionScopeNodes.isEmpty() )
314 {
315 mActionScopes
316 << QStringLiteral( "Canvas" )
317 << QStringLiteral( "Field" )
318 << QStringLiteral( "Feature" );
319 }
320 else
321 {
322 for ( int j = 0; j < actionScopeNodes.length(); ++j )
323 {
324 const QDomElement actionScopeElem = actionScopeNodes.item( j ).toElement();
325 mActionScopes << actionScopeElem.attribute( QStringLiteral( "id" ) );
326 }
327 }
328
329 mType = static_cast< QgsAction::ActionType >( actionElement.attributeNode( QStringLiteral( "type" ) ).value().toInt() );
330 mDescription = actionElement.attributeNode( QStringLiteral( "name" ) ).value();
331 mCommand = actionElement.attributeNode( QStringLiteral( "action" ) ).value();
332 mIcon = actionElement.attributeNode( QStringLiteral( "icon" ) ).value();
333 mCaptureOutput = actionElement.attributeNode( QStringLiteral( "capture" ) ).value().toInt() != 0;
334 mShortTitle = actionElement.attributeNode( QStringLiteral( "shortTitle" ) ).value();
335 mNotificationMessage = actionElement.attributeNode( QStringLiteral( "notificationMessage" ) ).value();
336 mIsEnabledOnlyWhenEditable = actionElement.attributeNode( QStringLiteral( "isEnabledOnlyWhenEditable" ) ).value().toInt() != 0;
337 mId = QUuid( actionElement.attributeNode( QStringLiteral( "id" ) ).value() );
338 if ( mId.isNull() )
339 mId = QUuid::createUuid();
340}
341
342void QgsAction::writeXml( QDomNode &actionsNode ) const
343{
344 QDomElement actionSetting = actionsNode.ownerDocument().createElement( QStringLiteral( "actionsetting" ) );
345 actionSetting.setAttribute( QStringLiteral( "type" ), mType );
346 actionSetting.setAttribute( QStringLiteral( "name" ), mDescription );
347 actionSetting.setAttribute( QStringLiteral( "shortTitle" ), mShortTitle );
348 actionSetting.setAttribute( QStringLiteral( "icon" ), mIcon );
349 actionSetting.setAttribute( QStringLiteral( "action" ), mCommand );
350 actionSetting.setAttribute( QStringLiteral( "capture" ), mCaptureOutput );
351 actionSetting.setAttribute( QStringLiteral( "notificationMessage" ), mNotificationMessage );
352 actionSetting.setAttribute( QStringLiteral( "isEnabledOnlyWhenEditable" ), mIsEnabledOnlyWhenEditable );
353 actionSetting.setAttribute( QStringLiteral( "id" ), mId.toString() );
354
355 const auto constMActionScopes = mActionScopes;
356 for ( const QString &scope : constMActionScopes )
357 {
358 QDomElement actionScopeElem = actionsNode.ownerDocument().createElement( QStringLiteral( "actionScope" ) );
359 actionScopeElem.setAttribute( QStringLiteral( "id" ), scope );
360 actionSetting.appendChild( actionScopeElem );
361 }
362
363 actionsNode.appendChild( actionSetting );
364}
365
367{
368 mExpressionContextScope = scope;
369}
370
372{
373 return mExpressionContextScope;
374}
375
376QString QgsAction::html() const
377{
378 QString typeText;
379 switch ( mType )
380 {
381 case Generic:
382 {
383 typeText = QObject::tr( "Generic" );
384 break;
385 }
386 case GenericPython:
387 {
388 typeText = QObject::tr( "Generic Python" );
389 break;
390 }
391 case Mac:
392 {
393 typeText = QObject::tr( "Mac" );
394 break;
395 }
396 case Windows:
397 {
398 typeText = QObject::tr( "Windows" );
399 break;
400 }
401 case Unix:
402 {
403 typeText = QObject::tr( "Unix" );
404 break;
405 }
406 case OpenUrl:
407 {
408 typeText = QObject::tr( "Open URL" );
409 break;
410 }
411 case SubmitUrlEncoded:
412 {
413 typeText = QObject::tr( "Submit URL (urlencoded or JSON)" );
414 break;
415 }
417 {
418 typeText = QObject::tr( "Submit URL (multipart)" );
419 break;
420 }
421 }
422 return { QObject::tr( R"html(
423<h2>Action Details</h2>
424<p>
425 <b>Description:</b> %1<br>
426 <b>Short title:</b> %2<br>
427 <b>Type:</b> %3<br>
428 <b>Scope:</b> %4<br>
429 <b>Action:</b><br>
430 <pre>%6</pre>
431</p>
432 )html" ).arg( mDescription, mShortTitle, typeText, actionScopes().values().join( QLatin1String( ", " ) ), mCommand )};
433};
QSet< QString > actionScopes() const
The action scopes define where an action will be available.
Definition: qgsaction.cpp:298
void readXml(const QDomNode &actionNode)
Reads an XML definition from actionNode into this object.
Definition: qgsaction.cpp:308
void run(QgsVectorLayer *layer, const QgsFeature &feature, const QgsExpressionContext &expressionContext) const
Run this action.
Definition: qgsaction.cpp:59
void setCommand(const QString &newCommand)
Sets the action command.
Definition: qgsaction.cpp:251
bool runable() const
Checks if the action is runable on the current platform.
Definition: qgsaction.cpp:42
bool isValid() const
Returns true if this action was a default constructed one.
Definition: qgsaction.h:144
void setExpressionContextScope(const QgsExpressionContextScope &scope)
Sets an expression context scope to use for running the action.
Definition: qgsaction.cpp:366
QString html() const
Returns an HTML table with the basic information about this action.
Definition: qgsaction.cpp:376
void writeXml(QDomNode &actionsNode) const
Appends an XML definition for this action as a new child node to actionsNode.
Definition: qgsaction.cpp:342
QgsExpressionContextScope expressionContextScope() const
Returns an expression context scope used for running the action.
Definition: qgsaction.cpp:371
void setActionScopes(const QSet< QString > &actionScopes)
The action scopes define where an action will be available.
Definition: qgsaction.cpp:303
@ GenericPython
Definition: qgsaction.h:40
@ SubmitUrlEncoded
POST data to an URL, using "application/x-www-form-urlencoded" or "application/json" if the body is v...
Definition: qgsaction.h:45
@ SubmitUrlMultipart
POST data to an URL using "multipart/form-data".
Definition: qgsaction.h:46
Single scope for storing variables and functions for use within a QgsExpressionContext.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
static QString replaceExpressionText(const QString &action, const QgsExpressionContext *context, const QgsDistanceArea *distanceArea=nullptr)
This function replaces each expression between [% and %] in the string with the result of its evaluat...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition: qgsfeature.h:56
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
static bool run(const QString &command, const QString &messageOnError=QString())
Execute a Python statement.
static QgsRunProcess * create(const QString &action, bool capture)
Definition: qgsrunprocess.h:59
Represents a vector layer which manages a vector based data sets.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38