Python plugins can also run on QGIS Server (see QGIS comme serveur de données OGC): by using the server interface (QgsServerInterface) a Python plugin running on the server can alter the behavior of existing core services (WMS, WFS etc.).
With the server filter interface (QgsServerFilter) we can change the input parameters, change the generated output or even by providing new services.
With the access control interface (QgsAccessControlFilter) we can apply some access restriction per requests.
Server python plugins are loaded once when the FCGI application starts. They register one or more QgsServerFilter (from this point, you might find useful a quick look to the server plugins API docs). Each filter should implement at least one of three callbacks:
Tous les filtres ont accès à l’objet de requête/réponse (QgsRequestHandler) et peuvent manipuler toutes les propriétés (entrée/sortie) et déclencher des exceptions (à l’aide d’une méthode un peu particulière comme nous le verrons ci-dessous).
Voici un pseudo-code présentant une session serveur typique et quand les fonctions de retour des filtres sont appelées:
Créer un gestionnaire de requête GET/POST/SOAP.
Passer la requête à une instance de la classe QgsServerInterface.
Appeler la fonction requestReady() des filtres d’extension.
Appeler la fonction du serveur executeRequest() et appeler la fonction des filtres d’extension sendResponse() lors de l’envoi du flux vers la sortie ou alors conserver le flux binaire et le type de contenu dans le gestionnaire de requête.
Appeler la fonction responseComplete() des filtres d’extension.
Appeler la fonction sendResponse() des filtres d’extension.
Demander au gestionnaire d’émettre la réponse.
Les paragraphes qui suivent décrivent les fonctions de rappel disponibles en détails.
Cette fonction est appelée lorsque la requête est prêt: l’URL entrante et ses données ont été analysées et juste avant de passer la main aux services principaux (WMS, WFS, etc.), c’est le point où vous pouvez manipuler l’entrée et dérouler des actions telles que:
l’authentification/l’autorisation
les redirections
l’ajout/suppression de certains paramètres (les noms de type par exemple)
le déclenchement d’exceptions
Vous pouvez également substituer l’intégralité d’un service principal en modifiant le paramètre SERVICE et complètement outrepasser le service (ce qui n’a pas beaucoup d’intérêt).
Cette fonction est appelée lorsque la sortie est envoyée vers la sortie FCGI ``stdout``(et ainsi, vers le client); cette action est habituellement réalisée après la fin du traitement des services principaux et après que le signal responseComplete ait été appelé. Dans un certain nombre de rares cas, le contenu XML peut devenir si volumineux qu’il a fallu implémenter une gestion de flux XML (GetFeature pour WFS est l’une d’entre elles) et dans ce cas, sendResponse() est appelée plusieurs fois avant que la réponse soit complète (et avant que responseComplete() soit appelée). La conséquence naturelle est que sendResponse() est normalement appelée une fois mais peut, exceptionnellement, être appelée plusieurs fois et dans ce cas (et uniquement dans ce cas), elle est appelée avant responseComplete().
sendResponse() est le meilleur moment pour la manipulation directe des sorties des services principaux et alors que la fonction responseComplete() est généralement une option, sendResponse() est la seule option viable pour le cas des services en flux.
Cette fonction est appelée lorsque les services principaux (si lancés) terminent leur processus et que la requête est prête à être envoyée au client. Comme indiqué ci-dessus, elle est normalement appelée avant sendResponse() sauf pour les services en flux (ou les filtres d’extension) qui peuvent avoir lancé sendResponse() plus tôt.
responseComplete() est le moment adéquat pour fournir de nouvelles implémentations de service (WPS ou services personnalisés) et pour effectuer des manipulations directes de la sortie des services principaux (comme par exemple pour ajouter un filigrane sur une image WMS).
Il reste encore du travail sur ce sujet: l’implémentation actuelle gère les exceptions gérées ou non en paramétrant la propriété QgsRequestHandler avec une instance de QgsMapServiceException. De cette manière, le code C++ peut capturer les exceptions Python gérées et ignorer les exceptions non gérées (ou mieux, les journaliser).
Cette approche fonctionne globalement mais elle n’est pas très “pythonesque”: une meilleure approche consisterait à déclencher des exceptions depuis le code Python et les faire remonter dans la boucle principale C++ pour y être traitées.
A server plugins is just a standard QGIS Python plugin as described in Développer des extensions Python, that just provides an additional (or alternative) interface: a typical QGIS desktop plugin has access to QGIS application through the QgisInterface instance, a server plugin has also access to a QgsServerInterface.
Pour indiquer à QGIS Server qu’une extension dispose d’une interface serveur, une métadonnée spécifique est requise (dans metadata.txt)
server=True
The example plugin discussed here (with many more example filters) is available on github: QGIS HelloServer Example Plugin
Vous pouvez voir ici la structure du répertoire de notre exemple d’extension pour serveur
PYTHON_PLUGINS_PATH/
HelloServer/
__init__.py --> *required*
HelloServer.py --> *required*
metadata.txt --> *required*
This file is required by Python’s import system. Also, QGIS Server requires that this file contains a serverClassFactory() function, which is called when the plugin gets loaded into QGIS Server when the server starts. It receives reference to instance of QgsServerInterface and must return instance of your plugin’s class. This is how the example plugin __init__.py looks like:
# -*- coding: utf-8 -*-
def serverClassFactory(serverIface):
from HelloServer import HelloServerServer
return HelloServerServer(serverIface)
C’est l’endroit où tout se passe et voici à quoi il devrait ressembler : (ex. HelloServer.py)
Une extension côté serveur consiste typiquement en une ou plusieurs fonctions de rappel empaquetées sous forme d’objets appelés QgsServerFilter.
Chaque QgsServerFilter implémente une ou plusieurs des fonctions de rappel suivantes:
L’exemple qui suit implémente un filtre minimaliste qui affiche HelloServer! pour le cas où le paramètre SERVICE vaut “HELLO”:
from qgis.server import *
from qgis.core import *
class HelloFilter(QgsServerFilter):
def __init__(self, serverIface):
super(HelloFilter, self).__init__(serverIface)
def responseComplete(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap()
if params.get('SERVICE', '').upper() == 'HELLO':
request.clearHeaders()
request.setHeader('Content-type', 'text/plain')
request.clearBody()
request.appendBody('HelloServer!')
Les filtres doivent être référencés dans la variable serverIface comme indiqué dans l’exemple suivant:
class HelloServerServer:
def __init__(self, serverIface):
# Save reference to the QGIS server interface
self.serverIface = serverIface
serverIface.registerFilter( HelloFilter, 100 )
Le second paramètre de registerFilter() permet de définir une priorité indiquant l’ordre des fonctions de rappel ayant le même nom (une priorité faible est invoquée en premier).
En utilisant les trois fonctions de rappel, les extensions peuvent manipuler l’entrée et/ou la sortie du serveur de plusieurs manières. A chaque instant, l’instance de l’extension a accès à la classe QgsRequestHandler au travers de la classe QgsServerInterface, QgsRequestHandler dispose de nombreuses méthodes qui peuvent être utilisée pour modifier les paramètres d’entrée avant qu’ils intègrent le processus principal du serveur (à l’aide de requestReady()) ou après que la requête ait été traitée par les services principaux (en utilisant sendResponse()).
Les exemples suivants montrent quelques cas d’utilisation courants :
L’extension d’exemple contient un test qui modifie les paramètres d’entrée provenant de la requête; dans cet exemple, un nouveau paramètre est injecté dans parameterMap (qui est déjà analysé), ce paramètre est alors visible dans les services principaux (WMS, etc.), à la fin du processus des services principaux, nous vérifions que le paramètre est toujours présent:
from qgis.server import *
from qgis.core import *
class ParamsFilter(QgsServerFilter):
def __init__(self, serverIface):
super(ParamsFilter, self).__init__(serverIface)
def requestReady(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap( )
request.setParameter('TEST_NEW_PARAM', 'ParamsFilter')
def responseComplete(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap( )
if params.get('TEST_NEW_PARAM') == 'ParamsFilter':
QgsMessageLog.logMessage("SUCCESS - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.INFO)
else:
QgsMessageLog.logMessage("FAIL - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.CRITICAL)
Ceci est un extrait de ce que vous pouvez voir dans le fichier log:
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter
src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded!
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded
src/mapserver/qgsgetrequesthandler.cpp: 35: (parseInput) [0ms] query string is: SERVICE=HELLO&request=GetOutput
src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map
src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [0ms] inserting pair REQUEST // GetOutput into the parameter map
src/mapserver/qgsserverfilter.cpp: 42: (requestReady) [0ms] QgsServerFilter plugin default requestReady called
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.requestReady
src/mapserver/qgis_map_serv.cpp: 235: (configPath) [0ms] Using default configuration file path: /home/xxx/apps/bin/admin.sld
src/mapserver/qgshttprequesthandler.cpp: 49: (setHttpResponse) [0ms] Checking byte array is ok to set...
src/mapserver/qgshttprequesthandler.cpp: 59: (setHttpResponse) [0ms] Byte array looks good, setting response...
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.responseComplete
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.responseComplete
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] RemoteConsoleFilter.responseComplete
src/mapserver/qgshttprequesthandler.cpp: 158: (sendResponse) [0ms] Sending HTTP response
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.sendResponse
Sur la ligne en surbrillance, la chaîne “SUCCESS” indique que le plugin a réussi le test.
La même technique peut être employée pour utiliser un service personnalisé à la place d’un service principal: vous pouviez par exemple sauter une requête WFS SERVICE ou n’importe quelle requête principale en modifiant le paramètre SERVICE par quelque-chose de différent et le service principal ne serait alors pas lancé; vous pourriez ensuite injecter vos resultats personnalisés dans la sortie et les renvoyer au client (ceci est expliqué ci-dessous).
L’exemple du filtre de filigrane montre comment remplacer la sortie WMS avec une nouvelle image obtenue par l’ajout d’un filigrane plaqué sur l’image WMS générée par le service principal WMS:
import os
from qgis.server import *
from qgis.core import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class WatermarkFilter(QgsServerFilter):
def __init__(self, serverIface):
super(WatermarkFilter, self).__init__(serverIface)
def responseComplete(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap( )
# Do some checks
if (request.parameter('SERVICE').upper() == 'WMS' \
and request.parameter('REQUEST').upper() == 'GETMAP' \
and not request.exceptionRaised() ):
QgsMessageLog.logMessage("WatermarkFilter.responseComplete: image ready %s" % request.infoFormat(), 'plugin', QgsMessageLog.INFO)
# Get the image
img = QImage()
img.loadFromData(request.body())
# Adds the watermark
watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png'))
p = QPainter(img)
p.drawImage(QRect( 20, 20, 40, 40), watermark)
p.end()
ba = QByteArray()
buffer = QBuffer(ba)
buffer.open(QIODevice.WriteOnly)
img.save(buffer, "PNG")
# Set the body
request.clearBody()
request.appendBody(ba)
Dans cet exemple, la valeur du paramètre SERVICE est vérifiée. Si la requête entrante est de type WMS GETMAP et qu’aucune exception n’a été déclarée par une extension déjà déclarée ou par un service principal (WMS dans notre cas), l’image WMS générée est récupérée depuis le tampon de sortie et l’image du filigrane est ajoutée. L’étape finale consiste à vider le tampon de sortie et à le remplacer par l’image nouvellement générée. Merci de prendre note que dans une situation réelle, nous devrions également vérifier le type d’image requêtée au lieu de retourner du PNG quoiqu’il arrive.
Voici l’arborescence de notre exemple d’extension serveur:
PYTHON_PLUGINS_PATH/
MyAccessControl/
__init__.py --> *required*
AccessControl.py --> *required*
metadata.txt --> *required*
This file is required by Python’s import system. As for all QGIS server plugins, this file contains a serverClassFactory() function, which is called when the plugin gets loaded into QGIS Server when the server starts. It receives reference to instance of QgsServerInterface and must return instance of your plugin’s class. This is how the example plugin __init__.py looks like:
# -*- coding: utf-8 -*-
def serverClassFactory(serverIface):
from MyAccessControl.AccessControl import AccessControl
return AccessControl(serverIface)
class AccessControl(QgsAccessControlFilter):
def __init__(self, server_iface):
super(QgsAccessControlFilter, self).__init__(server_iface)
def layerFilterExpression(self, layer):
""" Return an additional expression filter """
return super(QgsAccessControlFilter, self).layerFilterExpression(layer)
def layerFilterSubsetString(self, layer):
""" Return an additional subset string (typically SQL) filter """
return super(QgsAccessControlFilter, self).layerFilterSubsetString(layer)
def layerPermissions(self, layer):
""" Return the layer rights """
return super(QgsAccessControlFilter, self).layerPermissions(layer)
def authorizedLayerAttributes(self, layer, attributes):
""" Return the authorised layer attributes """
return super(QgsAccessControlFilter, self).authorizedLayerAttributes(layer, attributes)
def allowToEdit(self, layer, feature):
""" Are we authorise to modify the following geometry """
return super(QgsAccessControlFilter, self).allowToEdit(layer, feature)
def cacheKey(self):
return super(QgsAccessControlFilter, self).cacheKey()
Cet exemple donne un accès total à tout le monde.
C’est le rôle de l’extension de connaître qui est connecté dessus.
Pour toutes ces méthodes nous avons la couche passée en argument afin de personnaliser la restriction par couche.
Utilisé pour ajouter une expression pour limiter les résultats, ex:
def layerFilterExpression(self, layer):
return "$role = 'user'"
Pour limiter aux entités où l’attribut role vaut “user”.
Comme le point précédent mais utilise SubsetString (exécuté au niveau de la base de données).
def layerFilterSubsetString(self, layer):
return "role = 'user'"
Pour limiter aux entités où l’attribut role vaut “user”.
Limiter l’accès à la couche.
Renvoie un objet de type QgsAccessControlFilter.LayerPermissions qui dispose des propriétés suivantes:
canRead pour autoriser l’affichage dans les requêtes GetCapabilities et pour permettre l’accès en lecture.
canInsert pour autoriser l’insertion de nouvelles entités.
canUpdate pour autoriser les mises à jour d’entités.
candelete pour autoriser les suppressions d’entités.
Exemple :
def layerPermissions(self, layer):
rights = QgsAccessControlFilter.LayerPermissions()
rights.canRead = True
rights.canRead = rights.canInsert = rights.canUpdate = rights.canDelete = False
return rights
Pour tout limiter à un accès en lecture seule.
Utilisé pour limiter la visibilité d’un sous-groupe d’attribut spécifique.
L’argument attributes renvoie la liste des attributs réellement visibles.
Exemple :
def authorizedLayerAttributes(self, layer, attributes):
return [a for a in attributes if a != "role"]
Cache l’attribut ‘role’.
Il permet de limiter l’édition à un sous-ensemble d’entités.
Il est utilisé dans le protocole WFS-Transaction.
Exemple :
def allowToEdit(self, layer, feature):
return feature.attribute('role') == 'user'
Pour limiter l’édition aux entités dont l’attribut role contient la valeur user.