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