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