QGIS API Documentation 3.99.0-Master (21b3aa880ba)
Loading...
Searching...
No Matches
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
21#include "qgslogger.h"
22#include "qgsmessagelog.h"
24#include "qgspythonrunner.h"
25#include "qgsrunprocess.h"
26#include "qgsvariantutils.h"
27#include "qgsvectorlayer.h"
28
29#include <QApplication>
30#include <QDesktopServices>
31#include <QDir>
32#include <QFileInfo>
33#include <QHttpMultiPart>
34#include <QJsonDocument>
35#include <QMimeDatabase>
36#include <QNetworkRequest>
37#include <QTemporaryDir>
38#include <QUrl>
39#include <QUrlQuery>
40
42{
43// clang analyzer is not happy because of the multiple duplicate return branches, but this is ok :)
44// NOLINTBEGIN(bugprone-branch-clone)
45 switch ( mType )
46 {
52 return true;
53
54#if defined(Q_OS_WIN)
56 return true;
59 return false;
60#elif defined(Q_OS_MAC)
62 return true;
65 return false;
66#else
68 return true;
71 return false;
72#endif
73 }
74 return false;
75 // NOLINTEND(bugprone-branch-clone)
76}
77
78void QgsAction::run( QgsVectorLayer *layer, const QgsFeature &feature, const QgsExpressionContext &expressionContext ) const
79{
80 QgsExpressionContext actionContext( expressionContext );
81
82 actionContext << QgsExpressionContextUtils::layerScope( layer );
83 actionContext.setFeature( feature );
84
85 run( actionContext );
86}
87
88void QgsAction::handleFormSubmitAction( const QString &expandedAction ) const
89{
90
91 // Show busy in case the form subit is slow
92 QApplication::setOverrideCursor( Qt::WaitCursor );
93
94 QUrl url{ expandedAction };
95
96 // Encode '+' (fully encoded doesn't encode it)
97 const QString payload { url.query( QUrl::ComponentFormattingOption::FullyEncoded ).replace( QChar( '+' ), QStringLiteral( "%2B" ) ) };
98
99 // Remove query string from URL
100 const QUrlQuery queryString { url.query( ) };
101 url.setQuery( QString( ) );
102
103 QNetworkRequest req { url };
104
105 // Specific code for testing, produces an invalid POST but we can still listen to
106 // signals and examine the request
107 if ( url.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
108 {
109 req.setUrl( QStringLiteral( "file://%1" ).arg( url.path() ) );
110 }
111
112 QNetworkReply *reply = nullptr;
113
115 {
116 QString contentType { QStringLiteral( "application/x-www-form-urlencoded" ) };
117 // check for json
118 QJsonParseError jsonError;
119 QJsonDocument::fromJson( payload.toUtf8(), &jsonError );
120 if ( jsonError.error == QJsonParseError::ParseError::NoError )
121 {
122 contentType = QStringLiteral( "application/json" );
123 }
124 req.setHeader( QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType );
125 reply = QgsNetworkAccessManager::instance()->post( req, payload.toUtf8() );
126 }
127 // for multipart create parts and headers
128 else
129 {
130 QHttpMultiPart *multiPart = new QHttpMultiPart( QHttpMultiPart::FormDataType );
131 const QList<QPair<QString, QString>> queryItems { queryString.queryItems( QUrl::ComponentFormattingOption::FullyDecoded ) };
132 for ( const QPair<QString, QString> &queryItem : std::as_const( queryItems ) )
133 {
134 QHttpPart part;
135 part.setHeader( QNetworkRequest::ContentDispositionHeader,
136 QStringLiteral( "form-data; name=\"%1\"" )
137 .arg( QString( queryItem.first ).replace( '"', QLatin1String( R"(\")" ) ) ) );
138 part.setBody( queryItem.second.toUtf8() );
139 multiPart->append( part );
140 }
141 reply = QgsNetworkAccessManager::instance()->post( req, multiPart );
142 multiPart->setParent( reply );
143 }
144
145 QObject::connect( reply, &QNetworkReply::finished, reply, [ reply ]
146 {
147 if ( reply->error() == QNetworkReply::NoError )
148 {
149
150 if ( QgsVariantUtils::isNull( reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) ) )
151 {
152
153 const QByteArray replyData = reply->readAll();
154
155 QString filename { "download.bin" };
156 if ( const std::string header = reply->header( QNetworkRequest::KnownHeaders::ContentDispositionHeader ).toString().toStdString(); ! header.empty() )
157 {
158
159 // Extract filename dealing with ill formed headers with unquoted file names
160
161 std::string ascii;
162
163 const std::string q1 { R"(filename=)" };
164
165 if ( size_t pos = header.find( q1 ); pos != std::string::npos )
166 {
167
168 // Deal with ill formed headers with unquoted file names
169 if ( header.find( R"(filename=")" ) != std::string::npos )
170 {
171 pos++;
172 }
173
174 const size_t len = pos + q1.size();
175
176 const std::string q2 { R"(")" };
177 if ( size_t pos = header.find( q2, len ); pos != std::string::npos )
178 {
179 bool escaped = false;
180 while ( pos != std::string::npos && header[pos - 1] == '\\' )
181 {
182 pos = header.find( q2, pos + 1 );
183 escaped = true;
184 }
185 ascii = header.substr( len, pos - len );
186 if ( escaped )
187 {
188 std::string cleaned;
189 for ( size_t i = 0; i < ascii.size(); ++i )
190 {
191 if ( ascii[i] == '\\' )
192 {
193 if ( i > 0 && ascii[i - 1] == '\\' )
194 {
195 cleaned.push_back( ascii[i] );
196 }
197 }
198 else
199 {
200 cleaned.push_back( ascii[i] );
201 }
202 }
203 ascii = std::move( cleaned );
204 }
205 }
206 }
207
208 std::string utf8;
209
210 const std::string u { R"(UTF-8'')" };
211 if ( const size_t pos = header.find( u ); pos != std::string::npos )
212 {
213 utf8 = header.substr( pos + u.size() );
214 }
215
216 // Prefer ascii over utf8
217 if ( ascii.empty() )
218 {
219 if ( ! utf8.empty( ) )
220 {
221 filename = QString::fromStdString( utf8 );
222 }
223 }
224 else
225 {
226 filename = QString::fromStdString( ascii );
227 }
228 }
229 else if ( !QgsVariantUtils::isNull( reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ) ) )
230 {
231 QString contentTypeHeader { reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).toString() };
232 // Strip charset if any
233 if ( contentTypeHeader.contains( ';' ) )
234 {
235 contentTypeHeader = contentTypeHeader.left( contentTypeHeader.indexOf( ';' ) );
236 }
237
238 QMimeType mimeType { QMimeDatabase().mimeTypeForName( contentTypeHeader ) };
239 if ( mimeType.isValid() )
240 {
241 filename = QStringLiteral( "download.%1" ).arg( mimeType.preferredSuffix() );
242 }
243 }
244
245 QTemporaryDir tempDir;
246 tempDir.setAutoRemove( false );
247 tempDir.path();
248 const QString tempFilePath{ tempDir.path() + QDir::separator() + filename };
249 QFile tempFile{ tempFilePath };
250 if ( tempFile.open( QIODevice::WriteOnly ) )
251 {
252 tempFile.write( replyData );
253 tempFile.close();
254 QDesktopServices::openUrl( QUrl::fromLocalFile( tempFilePath ) );
255 }
256 else
257 {
258 QgsMessageLog::logMessage( QObject::tr( "Could not open temporary file for writing" ), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical );
259 }
260 }
261 else
262 {
263 QgsMessageLog::logMessage( QObject::tr( "Redirect is not supported!" ), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical );
264 }
265 }
266 else
267 {
268 QgsMessageLog::logMessage( reply->errorString(), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical );
269 }
270 reply->deleteLater();
271 QApplication::restoreOverrideCursor( );
272 } );
273
274}
275
276void QgsAction::setCommand( const QString &newCommand )
277{
278 mCommand = newCommand;
279}
280
281void QgsAction::run( const QgsExpressionContext &expressionContext ) const
282{
283 if ( !isValid() )
284 {
285 QgsDebugError( QStringLiteral( "Invalid action cannot be run" ) );
286 return;
287 }
288
289 QgsExpressionContextScope *scope = new QgsExpressionContextScope( mExpressionContextScope );
290 QgsExpressionContext context( expressionContext );
291 context << scope;
292
293 // Show busy in case the expression evaluation is slow
294 QApplication::setOverrideCursor( Qt::WaitCursor );
295 const QString expandedAction = QgsExpression::replaceExpressionText( mCommand, &context );
296 QApplication::restoreOverrideCursor();
297
299 {
300 const QFileInfo finfo( expandedAction );
301 if ( finfo.exists() && finfo.isFile() )
302 QDesktopServices::openUrl( QUrl::fromLocalFile( expandedAction ) );
303 else
304 QDesktopServices::openUrl( QUrl( expandedAction, QUrl::TolerantMode ) );
305 }
307 {
308 handleFormSubmitAction( expandedAction );
309 }
311 {
312 // TODO: capture output from QgsPythonRunner (like QgsRunProcess does)
313 QgsPythonRunner::run( expandedAction );
314 }
315 else
316 {
317 // The QgsRunProcess instance created by this static function
318 // deletes itself when no longer needed.
319#ifndef __clang_analyzer__
320 QgsRunProcess::create( expandedAction, mCaptureOutput );
321#endif
322 }
323}
324
325QSet<QString> QgsAction::actionScopes() const
326{
327 return mActionScopes;
328}
329
330void QgsAction::setActionScopes( const QSet<QString> &actionScopes )
331{
332 mActionScopes = actionScopes;
333}
334
335void QgsAction::readXml( const QDomNode &actionNode )
336{
337 QDomElement actionElement = actionNode.toElement();
338 const QDomNodeList actionScopeNodes = actionElement.elementsByTagName( QStringLiteral( "actionScope" ) );
339
340 if ( actionScopeNodes.isEmpty() )
341 {
342 mActionScopes
343 << QStringLiteral( "Canvas" )
344 << QStringLiteral( "Field" )
345 << QStringLiteral( "Feature" );
346 }
347 else
348 {
349 for ( int j = 0; j < actionScopeNodes.length(); ++j )
350 {
351 const QDomElement actionScopeElem = actionScopeNodes.item( j ).toElement();
352 mActionScopes << actionScopeElem.attribute( QStringLiteral( "id" ) );
353 }
354 }
355
356 mType = static_cast< Qgis::AttributeActionType >( actionElement.attributeNode( QStringLiteral( "type" ) ).value().toInt() );
357 mDescription = actionElement.attributeNode( QStringLiteral( "name" ) ).value();
358 mCommand = actionElement.attributeNode( QStringLiteral( "action" ) ).value();
359 mIcon = actionElement.attributeNode( QStringLiteral( "icon" ) ).value();
360 mCaptureOutput = actionElement.attributeNode( QStringLiteral( "capture" ) ).value().toInt() != 0;
361 mShortTitle = actionElement.attributeNode( QStringLiteral( "shortTitle" ) ).value();
362 mNotificationMessage = actionElement.attributeNode( QStringLiteral( "notificationMessage" ) ).value();
363 mIsEnabledOnlyWhenEditable = actionElement.attributeNode( QStringLiteral( "isEnabledOnlyWhenEditable" ) ).value().toInt() != 0;
364 mId = QUuid( actionElement.attributeNode( QStringLiteral( "id" ) ).value() );
365 if ( mId.isNull() )
366 mId = QUuid::createUuid();
367}
368
369void QgsAction::writeXml( QDomNode &actionsNode ) const
370{
371 QDomElement actionSetting = actionsNode.ownerDocument().createElement( QStringLiteral( "actionsetting" ) );
372 actionSetting.setAttribute( QStringLiteral( "type" ), static_cast< int >( mType ) );
373 actionSetting.setAttribute( QStringLiteral( "name" ), mDescription );
374 actionSetting.setAttribute( QStringLiteral( "shortTitle" ), mShortTitle );
375 actionSetting.setAttribute( QStringLiteral( "icon" ), mIcon );
376 actionSetting.setAttribute( QStringLiteral( "action" ), mCommand );
377 actionSetting.setAttribute( QStringLiteral( "capture" ), mCaptureOutput );
378 actionSetting.setAttribute( QStringLiteral( "notificationMessage" ), mNotificationMessage );
379 actionSetting.setAttribute( QStringLiteral( "isEnabledOnlyWhenEditable" ), mIsEnabledOnlyWhenEditable );
380 actionSetting.setAttribute( QStringLiteral( "id" ), mId.toString() );
381
382 const auto constMActionScopes = mActionScopes;
383 for ( const QString &scope : constMActionScopes )
384 {
385 QDomElement actionScopeElem = actionsNode.ownerDocument().createElement( QStringLiteral( "actionScope" ) );
386 actionScopeElem.setAttribute( QStringLiteral( "id" ), scope );
387 actionSetting.appendChild( actionScopeElem );
388 }
389
390 actionsNode.appendChild( actionSetting );
391}
392
394{
395 mExpressionContextScope = scope;
396}
397
399{
400 return mExpressionContextScope;
401}
402
403QString QgsAction::html() const
404{
405 QString typeText;
406 switch ( mType )
407 {
409 {
410 typeText = QObject::tr( "Generic" );
411 break;
412 }
414 {
415 typeText = QObject::tr( "Generic Python" );
416 break;
417 }
419 {
420 typeText = QObject::tr( "macOS" );
421 break;
422 }
424 {
425 typeText = QObject::tr( "Windows" );
426 break;
427 }
429 {
430 typeText = QObject::tr( "Unix" );
431 break;
432 }
434 {
435 typeText = QObject::tr( "Open URL" );
436 break;
437 }
439 {
440 typeText = QObject::tr( "Submit URL (urlencoded or JSON)" );
441 break;
442 }
444 {
445 typeText = QObject::tr( "Submit URL (multipart)" );
446 break;
447 }
448 }
449 return { QObject::tr( R"html(
450<h2>Action Details</h2>
451<p>
452 <b>Description:</b> %1<br>
453 <b>Short title:</b> %2<br>
454 <b>Type:</b> %3<br>
455 <b>Scope:</b> %4<br>
456 <b>Action:</b><br>
457 <pre>%6</pre>
458</p>
459 )html" ).arg( mDescription, mShortTitle, typeText, actionScopes().values().join( QLatin1String( ", " ) ), mCommand )};
460};
AttributeActionType
Attribute action types.
Definition qgis.h:4675
@ Mac
MacOS specific.
Definition qgis.h:4678
@ OpenUrl
Open URL action.
Definition qgis.h:4681
@ Unix
Unix specific.
Definition qgis.h:4680
@ SubmitUrlMultipart
POST data to an URL using "multipart/form-data".
Definition qgis.h:4683
@ Windows
Windows specific.
Definition qgis.h:4679
@ SubmitUrlEncoded
POST data to an URL, using "application/x-www-form-urlencoded" or "application/json" if the body is v...
Definition qgis.h:4682
@ Critical
Critical/error message.
Definition qgis.h:159
QSet< QString > actionScopes() const
The action scopes define where an action will be available.
void readXml(const QDomNode &actionNode)
Reads an XML definition from actionNode into this object.
void run(QgsVectorLayer *layer, const QgsFeature &feature, const QgsExpressionContext &expressionContext) const
Run this action.
Definition qgsaction.cpp:78
void setCommand(const QString &newCommand)
Sets the action command.
bool runable() const
Checks if the action is runable on the current platform.
Definition qgsaction.cpp:41
bool isValid() const
Returns true if this action was a default constructed one.
Definition qgsaction.h:130
void setExpressionContextScope(const QgsExpressionContextScope &scope)
Sets an expression context scope to use for running the action.
QString html() const
Returns an HTML table with the basic information about this action.
void writeXml(QDomNode &actionsNode) const
Appends an XML definition for this action as a new child node to actionsNode.
QgsExpressionContextScope expressionContextScope() const
Returns an expression context scope used for running the action.
void setActionScopes(const QSet< QString > &actionScopes)
The action scopes define where an action will be available.
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:58
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE())
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)
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
Represents a vector layer which manages a vector based dataset.
#define QgsDebugError(str)
Definition qgslogger.h:57