Extensions Python pour QGIS Server

Avertissement

Despite our constant efforts, information beyond this line may not be updated for QGIS 3. Refer to https://qgis.org/pyqgis/master for the python API documentation or, give a hand to update the chapters you know about. Thanks.

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) you can change the input parameters, change the generated output or even provide new services.

  • With the access control interface (QgsAccessControlFilter) you can apply some access restriction per requests.

Architecture des extensions de filtre serveur

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:

All filters have access to the request/response object (QgsRequestHandler) and can manipulate all its properties (input/output) and raise exceptions (while in a quite particular way as we’ll see below).

Voici un pseudo-code présentant une session serveur typique et quand les fonctions de retour des filtres sont appelées:

  • Récupérer la requête entrante
    • Créer un gestionnaire de requête GET/POST/SOAP.

    • pass request to an instance of QgsServerInterface

    • call plugins requestReady filters

    • S’il n’y a pas de réponse
      • Si SERVICE vaut WMS/WFS/WCS.
        • Créer un serveur WMS/WFS/WCS.
          • call server’s executeRequest and possibly call sendResponse plugin filters when streaming output or store the byte stream output and content type in the request handler

      • call plugins responseComplete filters

    • call plugins sendResponse filters

    • Demander au gestionnaire d’émettre la réponse.

Les paragraphes qui suivent décrivent les fonctions de rappel disponibles en détails.

requestReady

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).

sendResponse

This is called whenever output is sent to FCGI stdout (and from there, to the client), this is normally done after core services have finished their process and after responseComplete hook was called, but in a few cases XML can become so huge that a streaming XML implementation was needed (WFS GetFeature is one of them), in this case, sendResponse is called multiple times before the response is complete (and before responseComplete is called). The obvious consequence is that sendResponse is normally called once but might be exceptionally called multiple times and in that case (and only in that case) it is also called before responseComplete.

sendResponse is the best place for direct manipulation of core service’s output and while responseComplete is typically also an option, sendResponse is the only viable option in case of streaming services.

responseComplete

This is called once when core services (if hit) finish their process and the request is ready to be sent to the client. As discussed above, this is normally called before sendResponse except for streaming services (or other plugin filters) that might have called sendResponse earlier.

responseComplete is the ideal place to provide new services implementation (WPS or custom services) and to perform direct manipulation of the output coming from core services (for example to add a watermark upon a WMS image).

Déclencher une exception depuis une extension

Some work has still to be done on this topic: the current implementation can distinguish between handled and unhandled exceptions by setting a QgsRequestHandler property to an instance of QgsMapServiceException, this way the main C++ code can catch handled python exceptions and ignore unhandled exceptions (or better: log them).

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.

Écriture d’une extension serveur

A server plugin is 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. You could also find more examples at https://github.com/elpaso/qgis3-server-vagrant/tree/master/resources/web/plugins or browsing the QGIS plugins repository.

Fichiers de l’extension

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*

__init__.py

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)

HelloServer.py

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.

Each QgsServerFilter implements one or more of the following callbacks:

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 )

The second parameter of registerFilter sets a priority which defines the order for the callbacks with the same name (the lower priority is invoked first).

By using the three callbacks, plugins can manipulate the input and/or the output of the server in many different ways. In every moment, the plugin instance has access to the QgsRequestHandler through the QgsServerInterface. The QgsRequestHandler class has plenty of methods that can be used to alter the input parameters before entering the core processing of the server (by using requestReady()) or after the request has been processed by the core services (by using sendResponse()).

Les exemples suivants montrent quelques cas d’utilisation courants :

Modifier la couche en entrée

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).

Modifier ou remplacer la couche en sortie

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 qgis.PyQt.QtCore import *
from qgis.PyQt.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 {}".format(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.

Extension de contrôle d’accès

Fichiers de l’extension

Voici l’arborescence de notre exemple d’extension serveur:

PYTHON_PLUGINS_PATH/
  MyAccessControl/
    __init__.py    --> *required*
    AccessControl.py  --> *required*
    metadata.txt   --> *required*

__init__.py

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 at startup. It receives a reference to an instance of QgsServerInterface and must return an 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)

AccessControl.py

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.

layerFilterExpression

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 ».

layerFilterSubsetString

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 ».

layerPermissions

Limiter l’accès à la couche.

Return an object of type LayerPermissions, which has the properties:

  • canRead to see it in the GetCapabilities and have read access.

  • canInsert to be able to insert a new feature.

  • canUpdate to be able to update a feature.

  • canDelete to be able to delete a feature.

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.

authorizedLayerAttributes

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”.

allowToEdit

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.

cacheKey

QGIS Server conserve un cache du capabilties donc pour avoir un cache par rôle vous pouvez retourner le rôle dans cette méthode. Ou retourner None pour complètement désactiver le cache.