# HG changeset patch # User Goffi # Date 1504850525 -7200 # Node ID efbdca10f0fb0192dee7c3ce203fed2e18cd0eab # Parent 2098295747fd88b3d5b1de7ef3f6ce1ae44d2f83 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. diff -r 2098295747fd -r efbdca10f0fb sat_pubsub/backend.py --- 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) diff -r 2098295747fd -r efbdca10f0fb sat_pubsub/const.py --- 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" diff -r 2098295747fd -r efbdca10f0fb sat_pubsub/pgsql_storage.py --- 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 diff -r 2098295747fd -r efbdca10f0fb sat_pubsub/schema.py --- /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 . + +# --- + +# 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 [] diff -r 2098295747fd -r efbdca10f0fb sat_pubsub/tap.py --- 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()