QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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
7  email : [email protected]
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"
33 #include "qgsexpressioncontext.h"
34 #include "qgsvectorlayer.h"
35 #include "qgslogger.h"
37 #include "qgswebview.h"
39 #include "qgsmessagelog.h"
40 
41 
42 bool QgsAction::runable() const
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 
59 void 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 
69 void 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 ( reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).isNull() )
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  std::string ascii;
141  const std::string q1 { R"(filename=")" };
142  if ( const unsigned long pos = header.find( q1 ); pos != std::string::npos )
143  {
144  const unsigned long len = pos + q1.size();
145 
146  const std::string q2 { R"(")" };
147  if ( unsigned long pos = header.find( q2, len ); pos != std::string::npos )
148  {
149  bool escaped = false;
150  while ( pos != std::string::npos && header[pos - 1] == '\\' )
151  {
152  pos = header.find( q2, pos + 1 );
153  escaped = true;
154  }
155  ascii = header.substr( len, pos - len );
156  if ( escaped )
157  {
158  std::string cleaned;
159  for ( size_t i = 0; i < ascii.size(); ++i )
160  {
161  if ( ascii[i] == '\\' )
162  {
163  if ( i > 0 && ascii[i - 1] == '\\' )
164  {
165  cleaned.push_back( ascii[i] );
166  }
167  }
168  else
169  {
170  cleaned.push_back( ascii[i] );
171  }
172  }
173  ascii = cleaned;
174  }
175  }
176  }
177 
178  std::string utf8;
179 
180  const std::string u { R"(UTF-8'')" };
181  if ( const unsigned long pos = header.find( u ); pos != std::string::npos )
182  {
183  utf8 = header.substr( pos + u.size() );
184  }
185 
186  // Prefer ascii over utf8
187  if ( ascii.empty() )
188  {
189  if ( ! utf8.empty( ) )
190  {
191  filename = QString::fromStdString( utf8 );
192  }
193  }
194  else
195  {
196  filename = QString::fromStdString( ascii );
197  }
198  }
199  else if ( !reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).isNull() )
200  {
201  QString contentTypeHeader { reply->header( QNetworkRequest::KnownHeaders::ContentTypeHeader ).toString() };
202  // Strip charset if any
203  if ( contentTypeHeader.contains( ';' ) )
204  {
205  contentTypeHeader = contentTypeHeader.left( contentTypeHeader.indexOf( ';' ) );
206  }
207 
208  QMimeType mimeType { QMimeDatabase().mimeTypeForName( contentTypeHeader ) };
209  if ( mimeType.isValid() )
210  {
211  filename = QStringLiteral( "download.%1" ).arg( mimeType.preferredSuffix() );
212  }
213  }
214 
215  QTemporaryDir tempDir;
216  tempDir.setAutoRemove( false );
217  tempDir.path();
218  const QString tempFilePath{ tempDir.path() + QDir::separator() + filename };
219  QFile tempFile{ tempFilePath };
220  tempFile.open( QIODevice::WriteOnly );
221  tempFile.write( replyData );
222  tempFile.close();
223  QDesktopServices::openUrl( QUrl::fromLocalFile( tempFilePath ) );
224  }
225  else
226  {
227  QgsMessageLog::logMessage( QObject::tr( "Redirect is not supported!" ), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical );
228  }
229  }
230  else
231  {
232  QgsMessageLog::logMessage( reply->errorString(), QStringLiteral( "Form Submit Action" ), Qgis::MessageLevel::Critical );
233  }
234  reply->deleteLater();
235  QApplication::restoreOverrideCursor( );
236  } );
237 
238 }
239 
240 void QgsAction::setCommand( const QString &newCommand )
241 {
242  mCommand = newCommand;
243 }
244 
245 void QgsAction::run( const QgsExpressionContext &expressionContext ) const
246 {
247  if ( !isValid() )
248  {
249  QgsDebugMsg( QStringLiteral( "Invalid action cannot be run" ) );
250  return;
251  }
252 
253  QgsExpressionContextScope *scope = new QgsExpressionContextScope( mExpressionContextScope );
254  QgsExpressionContext context( expressionContext );
255  context << scope;
256 
257  // Show busy in case the expression evaluation is slow
258  QApplication::setOverrideCursor( Qt::WaitCursor );
259  const QString expandedAction = QgsExpression::replaceExpressionText( mCommand, &context );
260  QApplication::restoreOverrideCursor();
261 
262  if ( mType == QgsAction::OpenUrl )
263  {
264  const QFileInfo finfo( expandedAction );
265  if ( finfo.exists() && finfo.isFile() )
266  QDesktopServices::openUrl( QUrl::fromLocalFile( expandedAction ) );
267  else
268  QDesktopServices::openUrl( QUrl( expandedAction, QUrl::TolerantMode ) );
269  }
270  else if ( mType == QgsAction::SubmitUrlEncoded || mType == QgsAction::SubmitUrlMultipart )
271  {
272  handleFormSubmitAction( expandedAction );
273  }
274  else if ( mType == QgsAction::GenericPython )
275  {
276  // TODO: capture output from QgsPythonRunner (like QgsRunProcess does)
277  QgsPythonRunner::run( expandedAction );
278  }
279  else
280  {
281  // The QgsRunProcess instance created by this static function
282  // deletes itself when no longer needed.
283  QgsRunProcess::create( expandedAction, mCaptureOutput );
284  }
285 }
286 
287 QSet<QString> QgsAction::actionScopes() const
288 {
289  return mActionScopes;
290 }
291 
292 void QgsAction::setActionScopes( const QSet<QString> &actionScopes )
293 {
294  mActionScopes = actionScopes;
295 }
296 
297 void QgsAction::readXml( const QDomNode &actionNode )
298 {
299  QDomElement actionElement = actionNode.toElement();
300  const QDomNodeList actionScopeNodes = actionElement.elementsByTagName( QStringLiteral( "actionScope" ) );
301 
302  if ( actionScopeNodes.isEmpty() )
303  {
304  mActionScopes
305  << QStringLiteral( "Canvas" )
306  << QStringLiteral( "Field" )
307  << QStringLiteral( "Feature" );
308  }
309  else
310  {
311  for ( int j = 0; j < actionScopeNodes.length(); ++j )
312  {
313  const QDomElement actionScopeElem = actionScopeNodes.item( j ).toElement();
314  mActionScopes << actionScopeElem.attribute( QStringLiteral( "id" ) );
315  }
316  }
317 
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() );
327  if ( mId.isNull() )
328  mId = QUuid::createUuid();
329 }
330 
331 void QgsAction::writeXml( QDomNode &actionsNode ) const
332 {
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() );
343 
344  const auto constMActionScopes = mActionScopes;
345  for ( const QString &scope : constMActionScopes )
346  {
347  QDomElement actionScopeElem = actionsNode.ownerDocument().createElement( QStringLiteral( "actionScope" ) );
348  actionScopeElem.setAttribute( QStringLiteral( "id" ), scope );
349  actionSetting.appendChild( actionScopeElem );
350  }
351 
352  actionsNode.appendChild( actionSetting );
353 }
354 
356 {
357  mExpressionContextScope = scope;
358 }
359 
361 {
362  return mExpressionContextScope;
363 }
364 
365 QString QgsAction::html() const
366 {
367  QString typeText;
368  switch ( mType )
369  {
370  case Generic:
371  {
372  typeText = QObject::tr( "Generic" );
373  break;
374  }
375  case GenericPython:
376  {
377  typeText = QObject::tr( "Generic Python" );
378  break;
379  }
380  case Mac:
381  {
382  typeText = QObject::tr( "Mac" );
383  break;
384  }
385  case Windows:
386  {
387  typeText = QObject::tr( "Windows" );
388  break;
389  }
390  case Unix:
391  {
392  typeText = QObject::tr( "Unix" );
393  break;
394  }
395  case OpenUrl:
396  {
397  typeText = QObject::tr( "Open URL" );
398  break;
399  }
400  case SubmitUrlEncoded:
401  {
402  typeText = QObject::tr( "Submit URL (urlencoded or JSON)" );
403  break;
404  }
405  case SubmitUrlMultipart:
406  {
407  typeText = QObject::tr( "Submit URL (multipart)" );
408  break;
409  }
410  }
411  return { QObject::tr( R"html(
412 <h2>Action Details</h2>
413 <p>
414  <b>Description:</b> %1<br>
415  <b>Short title:</b> %2<br>
416  <b>Type:</b> %3<br>
417  <b>Scope:</b> %4<br>
418  <b>Action:</b><br>
419  <pre>%6</pre>
420 </p>
421  )html" ).arg( mDescription, mShortTitle, typeText, actionScopes().values().join( QLatin1String( ", " ) ), mCommand )};
422 };
QgsAction::setExpressionContextScope
void setExpressionContextScope(const QgsExpressionContextScope &scope)
Sets an expression context scope to use for running the action.
Definition: qgsaction.cpp:355
QgsAction::run
void run(QgsVectorLayer *layer, const QgsFeature &feature, const QgsExpressionContext &expressionContext) const
Run this action.
Definition: qgsaction.cpp:59
QgsExpressionContext
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
Definition: qgsexpressioncontext.h:406
qgsexpressioncontextutils.h
QgsAction::Generic
@ Generic
Definition: qgsaction.h:39
QgsAction::setActionScopes
void setActionScopes(const QSet< QString > &actionScopes)
The action scopes define where an action will be available.
Definition: qgsaction.cpp:292
QgsAction::OpenUrl
@ OpenUrl
Definition: qgsaction.h:44
QgsExpressionContextUtils::layerScope
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
Definition: qgsexpressioncontextutils.cpp:334
QgsDebugMsg
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsAction::isValid
bool isValid() const
Returns true if this action was a default constructed one.
Definition: qgsaction.h:144
QgsAction::Windows
@ Windows
Definition: qgsaction.h:42
QgsAction::Mac
@ Mac
Definition: qgsaction.h:41
QgsAction::html
QString html() const
Returns an HTML table with the basic information about this action.
Definition: qgsaction.cpp:365
qgsexpressioncontext.h
QgsRunProcess::create
static QgsRunProcess * create(const QString &action, bool capture)
Definition: qgsrunprocess.h:59
QgsAction::GenericPython
@ GenericPython
Definition: qgsaction.h:40
QgsAction::Unix
@ Unix
Definition: qgsaction.h:43
qgsnetworkaccessmanager.h
qgsaction.h
QgsMessageLog::logMessage
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).
Definition: qgsmessagelog.cpp:27
QgsAction::SubmitUrlMultipart
@ SubmitUrlMultipart
POST data to an URL using "multipart/form-data".
Definition: qgsaction.h:46
QgsNetworkAccessManager::instance
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
Definition: qgsnetworkaccessmanager.cpp:202
QgsAction::ActionType
ActionType
Definition: qgsaction.h:37
QgsExpressionContextScope
Single scope for storing variables and functions for use within a QgsExpressionContext....
Definition: qgsexpressioncontext.h:113
qgsvectorlayer.h
QgsAction::runable
bool runable() const
Checks if the action is runable on the current platform.
Definition: qgsaction.cpp:42
QgsAction::actionScopes
QSet< QString > actionScopes() const
The action scopes define where an action will be available.
Definition: qgsaction.cpp:287
QgsAction::setCommand
void setCommand(const QString &newCommand)
Sets the action command.
Definition: qgsaction.cpp:240
QgsVectorLayer
Represents a vector layer which manages a vector based data sets.
Definition: qgsvectorlayer.h:391
QgsAction::writeXml
void writeXml(QDomNode &actionsNode) const
Appends an XML definition for this action as a new child node to actionsNode.
Definition: qgsaction.cpp:331
qgsrunprocess.h
QgsPythonRunner::run
static bool run(const QString &command, const QString &messageOnError=QString())
Execute a Python statement.
Definition: qgspythonrunner.cpp:28
QgsAction::readXml
void readXml(const QDomNode &actionNode)
Reads an XML definition from actionNode into this object.
Definition: qgsaction.cpp:297
QgsFeature
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition: qgsfeature.h:55
qgswebview.h
qgslogger.h
QgsAction::SubmitUrlEncoded
@ SubmitUrlEncoded
POST data to an URL, using "application/x-www-form-urlencoded" or "application/json" if the body is v...
Definition: qgsaction.h:45
QgsAction::expressionContextScope
QgsExpressionContextScope expressionContextScope() const
Returns an expression context scope used for running the action.
Definition: qgsaction.cpp:360
qgspythonrunner.h
QgsExpression::replaceExpressionText
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...
Definition: qgsexpression.cpp:434
qgsmessagelog.h
QgsExpressionContext::setFeature
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
Definition: qgsexpressioncontext.cpp:525