changeset 352:efbdca10f0fb

schema: node schema implementation node schema is an experimental (not standard yet, protoXEP should follow) feature allowing to attach a data schema to a node. This commit implement it and method needed to retrieve/set a schema.
author Goffi <goffi@goffi.org>
date Fri, 08 Sep 2017 08:02:05 +0200
parents 2098295747fd
children 7c5d85c6fb3a
files sat_pubsub/backend.py sat_pubsub/const.py sat_pubsub/pgsql_storage.py sat_pubsub/schema.py sat_pubsub/tap.py
diffstat 5 files changed, 184 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/sat_pubsub/backend.py	Fri Sep 08 08:02:05 2017 +0200
+++ b/sat_pubsub/backend.py	Fri Sep 08 08:02:05 2017 +0200
@@ -527,7 +527,8 @@
         config['pubsub#node_type'] = nodeType
         config.update(options)
 
-        d = self.storage.createNode(nodeIdentifier, requestor, config, pep, recipient)
+        # TODO: handle schema on creation
+        d = self.storage.createNode(nodeIdentifier, requestor, config, None, pep, recipient)
         d.addCallback(lambda _: nodeIdentifier)
         return d
 
@@ -566,6 +567,41 @@
         return node.setConfiguration(options)
 
 
+    def getNodeSchema(self, nodeIdentifier, pep, recipient):
+        if not nodeIdentifier:
+            return defer.fail(error.NoRootNode())
+
+        d = self.storage.getNode(nodeIdentifier, pep, recipient)
+        d.addCallback(lambda node: node.getSchema())
+
+        return d
+
+
+    def setNodeSchema(self, nodeIdentifier, schema, requestor, pep, recipient):
+        """set or remove Schema of a node
+
+        @param NodeIdentifier(unicode): identifier of the pubusb node
+        @param schema(domish.Element, None): schema to set
+            None to remove schema
+        """
+        if not nodeIdentifier:
+            return defer.fail(error.NoRootNode())
+
+        d = self.storage.getNode(nodeIdentifier, pep, recipient)
+        d.addCallback(_getAffiliation, requestor)
+        d.addCallback(self._doSetNodeSchema, schema)
+        return d
+
+
+    def _doSetNodeSchema(self, result, schema):
+        node, affiliation = result
+
+        if affiliation != 'owner':
+            raise error.Forbidden()
+
+        return node.setSchema(schema)
+
+
     def getAffiliations(self, entity, nodeIdentifier, pep, recipient):
         return self.storage.getAffiliations(entity, nodeIdentifier, pep, recipient)
 
--- a/sat_pubsub/const.py	Fri Sep 08 08:02:05 2017 +0200
+++ b/sat_pubsub/const.py	Fri Sep 08 08:02:05 2017 +0200
@@ -56,6 +56,10 @@
 NS_ITEM_CONFIG = "http://jabber.org/protocol/pubsub#item-config"
 NS_ATOM = "http://www.w3.org/2005/Atom"
 NS_FORWARD = 'urn:xmpp:forward:0'
+NS_SCHEMA = 'https://salut-a-toi/protocol/schema:0'
+NS_SCHEMA_FORM = 'https://salut-a-toi/protocol/schema#schema:0'
+NS_SCHEMA_RESTRICT = 'https://salut-a-toi/protocol/schema#restrict:0'
+
 OPT_ACCESS_MODEL = 'pubsub#access_model'
 OPT_ROSTER_GROUPS_ALLOWED = 'pubsub#roster_groups_allowed'
 OPT_PERSIST_ITEMS = "pubsub#persist_items"
--- a/sat_pubsub/pgsql_storage.py	Fri Sep 08 08:02:05 2017 +0200
+++ b/sat_pubsub/pgsql_storage.py	Fri Sep 08 08:02:05 2017 +0200
@@ -147,7 +147,10 @@
                     const.OPT_ACCESS_MODEL:row[6],
                     const.OPT_PUBLISH_MODEL:row[7],
                     }
-            node = LeafNode(row[0], row[1], configuration)
+            schema = row[8]
+            if schema is not None:
+                schema = parseXml(schema)
+            node = LeafNode(row[0], row[1], configuration, schema)
             node.dbpool = self.dbpool
             return node
         elif row[2] == 'collection':
@@ -157,7 +160,7 @@
                     const.OPT_ACCESS_MODEL: row[6],
                     const.OPT_PUBLISH_MODEL:row[7],
                     }
-            node = CollectionNode(row[0], row[1], configuration)
+            node = CollectionNode(row[0], row[1], configuration, None)
             node.dbpool = self.dbpool
             return node
         else:
@@ -179,6 +182,7 @@
                                  send_last_published_item,
                                  access_model,
                                  publish_model,
+                                 schema::text,
                                  pep
                             FROM nodes
                             WHERE node_id=%s""",
@@ -198,6 +202,7 @@
                                           send_last_published_item,
                                           access_model,
                                           publish_model,
+                                          schema::text,
                                           pep
                                    FROM nodes
                                    WHERE node=%s""",
@@ -228,11 +233,11 @@
         d.addCallback(lambda results: [r[0] for r in results])
         return d
 
-    def createNode(self, nodeIdentifier, owner, config, pep, recipient=None):
+    def createNode(self, nodeIdentifier, owner, config, schema, pep, recipient=None):
         return self.dbpool.runInteraction(self._createNode, nodeIdentifier,
-                                           owner, config, pep, recipient)
+                                           owner, config, schema, pep, recipient)
 
-    def _createNode(self, cursor, nodeIdentifier, owner, config, pep, recipient):
+    def _createNode(self, cursor, nodeIdentifier, owner, config, schema, pep, recipient):
         if config['pubsub#node_type'] != 'leaf':
             raise error.NoCollections()
 
@@ -241,15 +246,16 @@
         try:
             cursor.execute("""INSERT INTO nodes
                               (node, node_type, persist_items,
-                               deliver_payloads, send_last_published_item, access_model, publish_model, pep)
+                               deliver_payloads, send_last_published_item, access_model, publish_model, schema, pep)
                               VALUES
-                              (%s, 'leaf', %s, %s, %s, %s, %s, %s)""",
+                              (%s, 'leaf', %s, %s, %s, %s, %s, %s, %s)""",
                            (nodeIdentifier,
                             config['pubsub#persist_items'],
                             config['pubsub#deliver_payloads'],
                             config['pubsub#send_last_published_item'],
                             config[const.OPT_ACCESS_MODEL],
                             config[const.OPT_PUBLISH_MODEL],
+                            schema,
                             recipient.userhost() if pep else None
                             )
                            )
@@ -398,10 +404,11 @@
 
     implements(iidavoll.INode)
 
-    def __init__(self, nodeDbId, nodeIdentifier, config):
+    def __init__(self, nodeDbId, nodeIdentifier, config, schema):
         self.nodeDbId = nodeDbId
         self.nodeIdentifier = nodeIdentifier
         self._config = config
+        self._schema = schema
 
     def _checkNodeExists(self, cursor):
         cursor.execute("""SELECT 1 as exist FROM nodes WHERE node_id=%s""",
@@ -449,6 +456,24 @@
     def _setCachedConfiguration(self, void, config):
         self._config = config
 
+    def getSchema(self):
+        return self._schema
+
+    def setSchema(self, schema):
+        d = self.dbpool.runInteraction(self._setSchema, schema)
+        d.addCallback(self._setCachedSchema, schema)
+        return d
+
+    def _setSchema(self, cursor, schema):
+        self._checkNodeExists(cursor)
+        cursor.execute("""UPDATE nodes SET schema=%s
+                          WHERE node_id=%s""",
+                       (schema.toXml() if schema else None,
+                        self.nodeDbId))
+
+    def _setCachedSchema(self, void, schema):
+        self._schema = schema
+
     def getMetaData(self):
         config = copy.copy(self._config)
         config["pubsub#node_type"] = self.nodeType
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat_pubsub/schema.py	Fri Sep 08 08:02:05 2017 +0200
@@ -0,0 +1,106 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+#
+# Copyright (c) 2015 Jérôme Poisson
+
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# ---
+
+# This module implements node schema
+
+from twisted.words.protocols.jabber import jid
+from twisted.words.xish import domish
+from wokkel import disco, iwokkel
+from wokkel.iwokkel import IPubSubService
+from wokkel.subprotocols import XMPPHandler, IQHandlerMixin
+from wokkel import data_form, pubsub
+from zope.interface import implements
+from sat_pubsub import const
+
+QUERY_SCHEMA = "/pubsub[@xmlns='" + const.NS_SCHEMA + "']"
+
+
+class SchemaHandler(XMPPHandler, IQHandlerMixin):
+    implements(iwokkel.IDisco)
+    iqHandlers = {"/iq[@type='get']" + QUERY_SCHEMA: 'onSchemaGet',
+                  "/iq[@type='set']" + QUERY_SCHEMA: 'onSchemaSet'}
+
+    def __init__(self):
+        super(SchemaHandler, self).__init__()
+        self.pubsub_service = None
+
+    def connectionInitialized(self):
+        for handler in self.parent.handlers:
+            if IPubSubService.providedBy(handler):
+                self.pubsub_service = handler
+                break
+        self.backend = self.parent.parent.getServiceNamed('backend')
+        self.xmlstream.addObserver("/iq[@type='get' or @type='set']" + QUERY_SCHEMA, self.handleRequest)
+
+    def _getNodeSchemaCb(self, x_elt, nodeIdentifier):
+        schema_elt = domish.Element((const.NS_SCHEMA, 'schema'))
+        schema_elt['node'] = nodeIdentifier
+        if x_elt is not None:
+            assert x_elt.uri == u'jabber:x:data'
+            schema_elt.addChild(x_elt)
+        return schema_elt
+
+    def onSchemaGet(self, iq_elt):
+        try:
+            schema_elt = next(iq_elt.pubsub.elements(const.NS_SCHEMA, 'schema'))
+            nodeIdentifier = schema_elt['node']
+        except StopIteration:
+            raise pubsub.BadRequest(text='missing schema element')
+        except KeyError:
+            raise pubsub.BadRequest(text='missing node')
+        pep = iq_elt.delegated
+        recipient = jid.JID(iq_elt['to'])
+        d = self.backend.getNodeSchema(nodeIdentifier,
+                                       pep,
+                                       recipient)
+        d.addCallback(self._getNodeSchemaCb, nodeIdentifier)
+        return d.addErrback(self.pubsub_service.resource._mapErrors)
+
+    def onSchemaSet(self, iq_elt):
+        try:
+            schema_elt = next(iq_elt.pubsub.elements(const.NS_SCHEMA, 'schema'))
+            nodeIdentifier = schema_elt['node']
+        except StopIteration:
+            raise pubsub.BadRequest(text='missing schema element')
+        except KeyError:
+            raise pubsub.BadRequest(text='missing node')
+        requestor = jid.JID(iq_elt['from'])
+        pep = iq_elt.delegated
+        recipient = jid.JID(iq_elt['to'])
+        for x_elt in schema_elt.elements(data_form.NS_X_DATA, u'x'):
+            schema_form = data_form.Form.fromElement(x_elt)
+            if schema_form.formNamespace == const.NS_SCHEMA_FORM:
+                break
+        else:
+            # no schema form has been found
+            x_elt = None
+        d = self.backend.setNodeSchema(nodeIdentifier,
+                                       x_elt,
+                                       requestor,
+                                       pep,
+                                       recipient)
+        return d.addErrback(self.pubsub_service.resource._mapErrors)
+
+    def getDiscoInfo(self, requestor, service, nodeIdentifier=''):
+        return [disco.DiscoFeature(const.NS_SCHEMA)]
+
+    def getDiscoItems(self, requestor, service, nodeIdentifier=''):
+        return []
--- a/sat_pubsub/tap.py	Fri Sep 08 08:02:05 2017 +0200
+++ b/sat_pubsub/tap.py	Fri Sep 08 08:02:05 2017 +0200
@@ -68,6 +68,7 @@
 from sat_pubsub import const
 from sat_pubsub import mam as pubsub_mam
 from sat_pubsub.backend import BackendService
+from sat_pubsub.schema import SchemaHandler
 from sat_pubsub.privilege import PrivilegesHandler
 from sat_pubsub.delegation import DelegationsHandler
 
@@ -172,6 +173,9 @@
         mam_s.addFilter(data_form.Field(var=const.MAM_FILTER_CATEGORY))
         mam_s.setHandlerParent(cs)
 
+    sh = SchemaHandler()
+    sh.setHandlerParent(cs)
+
     # XXX: delegation must be instancied at the end,
     #      because it does some MonkeyPatching on handlers
     dh = DelegationsHandler()