1 /***************************************************************************
2                                qgsactionmanager.cpp
3 
4  A class that stores and controls the management and execution of actions
5  associated. Actions are defined to be external programs that are run
6  with user-specified inputs that can depend on the value of layer
7  attributes.
8 
9                              -------------------
10     begin                : Oct 24 2004
11     copyright            : (C) 2004 by Gavin Macaulay
12     email                : gavin at macaulay dot co dot nz
13 
14  ***************************************************************************/
15 
16 /***************************************************************************
17  *                                                                         *
18  *   This program is free software; you can redistribute it and/or modify  *
19  *   it under the terms of the GNU General Public License as published by  *
20  *   the Free Software Foundation; either version 2 of the License, or     *
21  *   (at your option) any later version.                                   *
22  *                                                                         *
23  ***************************************************************************/
24 
25 #include "qgsactionmanager.h"
26 #include "qgspythonrunner.h"
27 #include "qgsrunprocess.h"
28 #include "qgsvectorlayer.h"
29 #include "qgsproject.h"
30 #include "qgslogger.h"
31 #include "qgsexpression.h"
32 #include "qgsdataprovider.h"
33 #include "qgsexpressioncontextutils.h"
34 
35 #include <QList>
36 #include <QStringList>
37 #include <QDomElement>
38 #include <QSettings>
39 #include <QDesktopServices>
40 #include <QUrl>
41 #include <QDir>
42 #include <QFileInfo>
43 #include <QRegularExpression>
44 
45 
addAction(QgsAction::ActionType type,const QString & name,const QString & command,bool capture)46 QUuid QgsActionManager::addAction( QgsAction::ActionType type, const QString &name, const QString &command, bool capture )
47 {
48   QgsAction action( type, name, command, capture );
49   addAction( action );
50   return action.id();
51 }
52 
addAction(QgsAction::ActionType type,const QString & name,const QString & command,const QString & icon,bool capture)53 QUuid QgsActionManager::addAction( QgsAction::ActionType type, const QString &name, const QString &command, const QString &icon, bool capture )
54 {
55   QgsAction action( type, name, command, icon, capture );
56   addAction( action );
57   return action.id();
58 }
59 
addAction(const QgsAction & action)60 void QgsActionManager::addAction( const QgsAction &action )
61 {
62   QgsDebugMsg( "add action " + action.name() );
63   mActions.append( action );
64   if ( mLayer && mLayer->dataProvider() && !action.notificationMessage().isEmpty() )
65   {
66     mLayer->dataProvider()->setListening( true );
67     if ( !mOnNotifyConnected )
68     {
69       QgsDebugMsg( QStringLiteral( "connecting to notify" ) );
70       connect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions );
71       mOnNotifyConnected = true;
72     }
73   }
74 }
75 
onNotifyRunActions(const QString & message)76 void QgsActionManager::onNotifyRunActions( const QString &message )
77 {
78   for ( const QgsAction &act : std::as_const( mActions ) )
79   {
80     if ( !act.notificationMessage().isEmpty() && QRegularExpression( act.notificationMessage() ).match( message ).hasMatch() )
81     {
82       if ( !act.isValid() || !act.runable() )
83         continue;
84 
85       QgsExpressionContext context = createExpressionContext();
86 
87       Q_ASSERT( mLayer ); // if there is no layer, then where is the notification coming from ?
88       context << QgsExpressionContextUtils::layerScope( mLayer );
89       context << QgsExpressionContextUtils::notificationScope( message );
90 
91       QString expandedAction = QgsExpression::replaceExpressionText( act.command(), &context );
92       if ( expandedAction.isEmpty() )
93         continue;
94       runAction( QgsAction( act.type(), act.name(), expandedAction, act.capture() ) );
95     }
96   }
97 }
98 
removeAction(QUuid actionId)99 void QgsActionManager::removeAction( QUuid actionId )
100 {
101   int i = 0;
102   for ( const QgsAction &action : std::as_const( mActions ) )
103   {
104     if ( action.id() == actionId )
105     {
106       mActions.removeAt( i );
107       break;
108     }
109     ++i;
110   }
111 
112   if ( mOnNotifyConnected )
113   {
114     bool hasActionOnNotify = false;
115     for ( const QgsAction &action : std::as_const( mActions ) )
116       hasActionOnNotify |= !action.notificationMessage().isEmpty();
117     if ( !hasActionOnNotify && mLayer && mLayer->dataProvider() )
118     {
119       // note that there is no way of knowing if the provider is listening only because
120       // this class has hasked it to, so we do not reset the provider listening state here
121       disconnect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions );
122       mOnNotifyConnected = false;
123     }
124   }
125 }
126 
doAction(QUuid actionId,const QgsFeature & feature,int defaultValueIndex,const QgsExpressionContextScope & scope)127 void QgsActionManager::doAction( QUuid actionId, const QgsFeature &feature, int defaultValueIndex, const QgsExpressionContextScope &scope )
128 {
129   QgsExpressionContext context = createExpressionContext();
130   QgsExpressionContextScope *actionScope = new QgsExpressionContextScope( scope );
131   actionScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "field_index" ), defaultValueIndex, true ) );
132   if ( defaultValueIndex >= 0 && defaultValueIndex < feature.fields().size() )
133     actionScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "field_name" ), feature.fields().at( defaultValueIndex ).name(), true ) );
134   actionScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "field_value" ), feature.attribute( defaultValueIndex ), true ) );
135   context << actionScope;
136   doAction( actionId, feature, context );
137 }
138 
doAction(QUuid actionId,const QgsFeature & feat,const QgsExpressionContext & context)139 void QgsActionManager::doAction( QUuid actionId, const QgsFeature &feat, const QgsExpressionContext &context )
140 {
141   QgsAction act = action( actionId );
142 
143   if ( !act.isValid() || !act.runable() )
144     return;
145 
146   QgsExpressionContext actionContext( context );
147 
148   if ( mLayer )
149     actionContext << QgsExpressionContextUtils::layerScope( mLayer );
150   actionContext.setFeature( feat );
151 
152   QString expandedAction = QgsExpression::replaceExpressionText( act.command(), &actionContext );
153   if ( expandedAction.isEmpty() )
154     return;
155 
156   QgsAction newAction( act.type(), act.name(), expandedAction, act.capture() );
157   runAction( newAction );
158 }
159 
clearActions()160 void QgsActionManager::clearActions()
161 {
162   mActions.clear();
163   if ( mOnNotifyConnected && mLayer && mLayer->dataProvider() )
164   {
165     // note that there is no way of knowing if the provider is listening only because
166     // this class has hasked it to, so we do not reset the provider listening state here
167     disconnect( mLayer->dataProvider(), &QgsDataProvider::notify, this, &QgsActionManager::onNotifyRunActions );
168     mOnNotifyConnected = false;
169   }
170 }
171 
actions(const QString & actionScope) const172 QList<QgsAction> QgsActionManager::actions( const QString &actionScope ) const
173 {
174   if ( actionScope.isNull() )
175     return mActions;
176   else
177   {
178     QList<QgsAction> actions;
179 
180     for ( const QgsAction &action : std::as_const( mActions ) )
181     {
182       if ( action.actionScopes().contains( actionScope ) )
183         actions.append( action );
184     }
185 
186     return actions;
187   }
188 }
189 
runAction(const QgsAction & action)190 void QgsActionManager::runAction( const QgsAction &action )
191 {
192   if ( action.type() == QgsAction::OpenUrl )
193   {
194     QFileInfo finfo( action.command() );
195     if ( finfo.exists() && finfo.isFile() )
196       QDesktopServices::openUrl( QUrl::fromLocalFile( action.command() ) );
197     else
198       QDesktopServices::openUrl( QUrl( action.command(), QUrl::TolerantMode ) );
199   }
200   else if ( action.type() == QgsAction::GenericPython )
201   {
202     // TODO: capture output from QgsPythonRunner (like QgsRunProcess does)
203     QgsPythonRunner::run( action.command() );
204   }
205   else
206   {
207     // The QgsRunProcess instance created by this static function
208     // deletes itself when no longer needed.
209     QgsRunProcess::create( action.command(), action.capture() );
210   }
211 }
212 
createExpressionContext() const213 QgsExpressionContext QgsActionManager::createExpressionContext() const
214 {
215   QgsExpressionContext context;
216   context << QgsExpressionContextUtils::globalScope()
217           << QgsExpressionContextUtils::projectScope( QgsProject::instance() );
218   if ( mLayer )
219     context << QgsExpressionContextUtils::layerScope( mLayer );
220 
221   return context;
222 }
223 
writeXml(QDomNode & layer_node) const224 bool QgsActionManager::writeXml( QDomNode &layer_node ) const
225 {
226   QDomElement aActions = layer_node.ownerDocument().createElement( QStringLiteral( "attributeactions" ) );
227   for ( QMap<QString, QUuid>::const_iterator defaultAction = mDefaultActions.constBegin(); defaultAction != mDefaultActions.constEnd(); ++ defaultAction )
228   {
229     QDomElement defaultActionElement = layer_node.ownerDocument().createElement( QStringLiteral( "defaultAction" ) );
230     defaultActionElement.setAttribute( QStringLiteral( "key" ), defaultAction.key() );
231     defaultActionElement.setAttribute( QStringLiteral( "value" ), defaultAction.value().toString() );
232     aActions.appendChild( defaultActionElement );
233   }
234 
235   for ( const QgsAction &action : std::as_const( mActions ) )
236   {
237     action.writeXml( aActions );
238   }
239   layer_node.appendChild( aActions );
240 
241   return true;
242 }
243 
readXml(const QDomNode & layer_node)244 bool QgsActionManager::readXml( const QDomNode &layer_node )
245 {
246   clearActions();
247 
248   QDomNode aaNode = layer_node.namedItem( QStringLiteral( "attributeactions" ) );
249 
250   if ( !aaNode.isNull() )
251   {
252     QDomNodeList actionsettings = aaNode.toElement().elementsByTagName( QStringLiteral( "actionsetting" ) );
253     for ( int i = 0; i < actionsettings.size(); ++i )
254     {
255       QgsAction action;
256       action.readXml( actionsettings.item( i ) );
257       addAction( action );
258     }
259 
260     QDomNodeList defaultActionNodes = aaNode.toElement().elementsByTagName( QStringLiteral( "defaultAction" ) );
261 
262     for ( int i = 0; i < defaultActionNodes.size(); ++i )
263     {
264       QDomElement defaultValueElem = defaultActionNodes.at( i ).toElement();
265       mDefaultActions.insert( defaultValueElem.attribute( QStringLiteral( "key" ) ), QUuid( defaultValueElem.attribute( QStringLiteral( "value" ) ) ) );
266     }
267   }
268   return true;
269 }
270 
action(QUuid id) const271 QgsAction QgsActionManager::action( QUuid id ) const
272 {
273   for ( const QgsAction &action : std::as_const( mActions ) )
274   {
275     if ( action.id() == id )
276       return action;
277   }
278 
279   return QgsAction();
280 }
281 
setDefaultAction(const QString & actionScope,QUuid actionId)282 void QgsActionManager::setDefaultAction( const QString &actionScope, QUuid actionId )
283 {
284   mDefaultActions[ actionScope ] = actionId;
285 }
286 
defaultAction(const QString & actionScope)287 QgsAction QgsActionManager::defaultAction( const QString &actionScope )
288 {
289   return action( mDefaultActions.value( actionScope ) );
290 }
291