QGIS API Documentation  3.25.0-Master (dec16ba68b)
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::run( const QgsExpressionContext &expressionContext ) const
241 {
242  if ( !isValid() )
243  {
244  QgsDebugMsg( QStringLiteral( "Invalid action cannot be run" ) );
245  return;
246  }
247 
248  QgsExpressionContextScope *scope = new QgsExpressionContextScope( mExpressionContextScope );
249  QgsExpressionContext context( expressionContext );
250  context << scope;
251 
252  // Show busy in case the expression evaluation is slow
253  QApplication::setOverrideCursor( Qt::WaitCursor );
254  const QString expandedAction = QgsExpression::replaceExpressionText( mCommand, &context );
255  QApplication::restoreOverrideCursor();
256 
257  if ( mType == QgsAction::OpenUrl )
258  {
259  const QFileInfo finfo( expandedAction );
260  if ( finfo.exists() && finfo.isFile() )
261  QDesktopServices::openUrl( QUrl::fromLocalFile( expandedAction ) );
262  else
263  QDesktopServices::openUrl( QUrl( expandedAction, QUrl::TolerantMode ) );
264  }
265  else if ( mType == QgsAction::SubmitUrlEncoded || mType == QgsAction::SubmitUrlMultipart )
266  {
267  handleFormSubmitAction( expandedAction );
268  }
269  else if ( mType == QgsAction::GenericPython )
270  {
271  // TODO: capture output from QgsPythonRunner (like QgsRunProcess does)
272  QgsPythonRunner::run( expandedAction );
273  }
274  else
275  {
276  // The QgsRunProcess instance created by this static function
277  // deletes itself when no longer needed.
278  QgsRunProcess::create( expandedAction, mCaptureOutput );
279  }
280 }
281 
282 QSet<QString> QgsAction::actionScopes() const
283 {
284  return mActionScopes;
285 }
286 
287 void QgsAction::setActionScopes( const QSet<QString> &actionScopes )
288 {
289  mActionScopes = actionScopes;
290 }
291 
292 void QgsAction::readXml( const QDomNode &actionNode )
293 {
294  QDomElement actionElement = actionNode.toElement();
295  const QDomNodeList actionScopeNodes = actionElement.elementsByTagName( QStringLiteral( "actionScope" ) );
296 
297  if ( actionScopeNodes.isEmpty() )
298  {
299  mActionScopes
300  << QStringLiteral( "Canvas" )
301  << QStringLiteral( "Field" )
302  << QStringLiteral( "Feature" );
303  }
304  else
305  {
306  for ( int j = 0; j < actionScopeNodes.length(); ++j )
307  {
308  const QDomElement actionScopeElem = actionScopeNodes.item( j ).toElement();
309  mActionScopes << actionScopeElem.attribute( QStringLiteral( "id" ) );
310  }
311  }
312 
313  mType = static_cast< QgsAction::ActionType >( actionElement.attributeNode( QStringLiteral( "type" ) ).value().toInt() );
314  mDescription = actionElement.attributeNode( QStringLiteral( "name" ) ).value();
315  mCommand = actionElement.attributeNode( QStringLiteral( "action" ) ).value();
316  mIcon = actionElement.attributeNode( QStringLiteral( "icon" ) ).value();
317  mCaptureOutput = actionElement.attributeNode( QStringLiteral( "capture" ) ).value().toInt() != 0;
318  mShortTitle = actionElement.attributeNode( QStringLiteral( "shortTitle" ) ).value();
319  mNotificationMessage = actionElement.attributeNode( QStringLiteral( "notificationMessage" ) ).value();
320  mIsEnabledOnlyWhenEditable = actionElement.attributeNode( QStringLiteral( "isEnabledOnlyWhenEditable" ) ).value().toInt() != 0;
321  mId = QUuid( actionElement.attributeNode( QStringLiteral( "id" ) ).value() );
322  if ( mId.isNull() )
323  mId = QUuid::createUuid();
324 }
325 
326 void QgsAction::writeXml( QDomNode &actionsNode ) const
327 {
328  QDomElement actionSetting = actionsNode.ownerDocument().createElement( QStringLiteral( "actionsetting" ) );
329  actionSetting.setAttribute( QStringLiteral( "type" ), mType );
330  actionSetting.setAttribute( QStringLiteral( "name" ), mDescription );
331  actionSetting.setAttribute( QStringLiteral( "shortTitle" ), mShortTitle );
332  actionSetting.setAttribute( QStringLiteral( "icon" ), mIcon );
333  actionSetting.setAttribute( QStringLiteral( "action" ), mCommand );
334  actionSetting.setAttribute( QStringLiteral( "capture" ), mCaptureOutput );
335  actionSetting.setAttribute( QStringLiteral( "notificationMessage" ), mNotificationMessage );
336  actionSetting.setAttribute( QStringLiteral( "isEnabledOnlyWhenEditable" ), mIsEnabledOnlyWhenEditable );
337  actionSetting.setAttribute( QStringLiteral( "id" ), mId.toString() );
338 
339  const auto constMActionScopes = mActionScopes;
340  for ( const QString &scope : constMActionScopes )
341  {
342  QDomElement actionScopeElem = actionsNode.ownerDocument().createElement( QStringLiteral( "actionScope" ) );
343  actionScopeElem.setAttribute( QStringLiteral( "id" ), scope );
344  actionSetting.appendChild( actionScopeElem );
345  }
346 
347  actionsNode.appendChild( actionSetting );
348 }
349 
351 {
352  mExpressionContextScope = scope;
353 }
354 
356 {
357  return mExpressionContextScope;
358 }
359 
360 QString QgsAction::html() const
361 {
362  QString typeText;
363  switch ( mType )
364  {
365  case Generic:
366  {
367  typeText = QObject::tr( "Generic" );
368  break;
369  }
370  case GenericPython:
371  {
372  typeText = QObject::tr( "Generic Python" );
373  break;
374  }
375  case Mac:
376  {
377  typeText = QObject::tr( "Mac" );
378  break;
379  }
380  case Windows:
381  {
382  typeText = QObject::tr( "Windows" );
383  break;
384  }
385  case Unix:
386  {
387  typeText = QObject::tr( "Unix" );
388  break;
389  }
390  case OpenUrl:
391  {
392  typeText = QObject::tr( "Open URL" );
393  break;
394  }
395  case SubmitUrlEncoded:
396  {
397  typeText = QObject::tr( "Submit URL (urlencoded or JSON)" );
398  break;
399  }
400  case SubmitUrlMultipart:
401  {
402  typeText = QObject::tr( "Submit URL (multipart)" );
403  break;
404  }
405  }
406  return { QObject::tr( R"html(
407 <h2>Action Details</h2>
408 <p>
409  <b>Description:</b> %1<br>
410  <b>Short title:</b> %2<br>
411  <b>Type:</b> %3<br>
412  <b>Scope:</b> %4<br>
413  <b>Action:</b><br>
414  <pre>%6</pre>
415 </p>
416  )html" ).arg( mDescription, mShortTitle, typeText, actionScopes().values().join( QLatin1String( ", " ) ), mCommand )};
417 };
QSet< QString > actionScopes() const
The action scopes define where an action will be available.
Definition: qgsaction.cpp:282
void readXml(const QDomNode &actionNode)
Reads an XML definition from actionNode into this object.
Definition: qgsaction.cpp:292
void run(QgsVectorLayer *layer, const QgsFeature &feature, const QgsExpressionContext &expressionContext) const
Run this action.
Definition: qgsaction.cpp:59
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:350
QString html() const
Returns an HTML table with the basic information about this action.
Definition: qgsaction.cpp:360
void writeXml(QDomNode &actionsNode) const
Appends an XML definition for this action as a new child node to actionsNode.
Definition: qgsaction.cpp:326
QgsExpressionContextScope expressionContextScope() const
Returns an expression context scope used for running the action.
Definition: qgsaction.cpp:355
void setActionScopes(const QSet< QString > &actionScopes)
The action scopes define where an action will be available.
Definition: qgsaction.cpp:287
@ 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