they can be used to modify created widgets, this is specially useful to customise well known field.
308 lines
14 KiB
Python
Executable file
308 lines
14 KiB
Python
Executable file
#!/usr/bin/env python2
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# SAT plugin for Pubsub Schemas
|
|
# Copyright (C) 2009-2017 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# 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/>.
|
|
|
|
from sat.core.i18n import _
|
|
from sat.core import exceptions
|
|
from sat.core.constants import Const as C
|
|
from sat.tools import xml_tools
|
|
from twisted.words.protocols.jabber import jid
|
|
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
|
|
from twisted.internet import defer
|
|
from sat.core.log import getLogger
|
|
log = getLogger(__name__)
|
|
from wokkel import disco, iwokkel
|
|
from wokkel import data_form
|
|
from wokkel import generic
|
|
from zope.interface import implements
|
|
from collections import Iterable
|
|
import copy
|
|
|
|
NS_SCHEMA = 'https://salut-a-toi/protocol/schema:0'
|
|
|
|
PLUGIN_INFO = {
|
|
C.PI_NAME: "PubSub Schema",
|
|
C.PI_IMPORT_NAME: "PUBSUB_SCHEMA",
|
|
C.PI_TYPE: "EXP",
|
|
C.PI_PROTOCOLS: [],
|
|
C.PI_DEPENDENCIES: ["XEP-0060"],
|
|
C.PI_MAIN: "PubsubSchema",
|
|
C.PI_HANDLER: "yes",
|
|
C.PI_DESCRIPTION: _("""Handle Pubsub data schemas""")
|
|
}
|
|
|
|
|
|
class PubsubSchema(object):
|
|
|
|
def __init__(self, host):
|
|
log.info(_(u"PubSub Schema initialization"))
|
|
self.host = host
|
|
self._p = self.host.plugins["XEP-0060"]
|
|
host.bridge.addMethod("psSchemaGet", ".plugin",
|
|
in_sign='sss', out_sign='s',
|
|
method=self._getSchema,
|
|
async=True
|
|
)
|
|
host.bridge.addMethod("psSchemaSet", ".plugin",
|
|
in_sign='ssss', out_sign='',
|
|
method=self._setSchema,
|
|
async=True
|
|
)
|
|
host.bridge.addMethod("psSchemaUIGet", ".plugin",
|
|
in_sign='sss', out_sign='s',
|
|
method=self._getUISchema,
|
|
async=True
|
|
)
|
|
host.bridge.addMethod("psItemsFormGet", ".plugin",
|
|
in_sign='ssssiassa{ss}s', out_sign='(asa{ss})',
|
|
method=self._getDataFormItems,
|
|
async=True)
|
|
host.bridge.addMethod("psItemFormSend", ".plugin",
|
|
in_sign='ssa{sas}ssa{ss}s', out_sign='s',
|
|
method=self._sendDataFormItem,
|
|
async=True)
|
|
|
|
def getHandler(self, client):
|
|
return SchemaHandler()
|
|
|
|
def _getSchemaBridgeCb(self, schema_elt):
|
|
if schema_elt is None:
|
|
return u''
|
|
return schema_elt.toXml()
|
|
|
|
def _getSchema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE):
|
|
client = self.host.getClient(profile_key)
|
|
service = None if not service else jid.JID(service)
|
|
d = self.getSchema(client, service, nodeIdentifier)
|
|
d.addCallback(self._getSchemaBridgeCb)
|
|
return d
|
|
|
|
def _getSchemaCb(self, iq_elt):
|
|
try:
|
|
schema_elt = next(iq_elt.elements(NS_SCHEMA, 'schema'))
|
|
except StopIteration:
|
|
raise exceptions.DataError('missing <schema> element')
|
|
try:
|
|
x_elt = next(schema_elt.elements((data_form.NS_X_DATA, 'x')))
|
|
except StopIteration:
|
|
# there is not schema on this node
|
|
return None
|
|
return x_elt
|
|
|
|
def getSchema(self, client, service, nodeIdentifier):
|
|
"""retrieve PubSub node schema
|
|
|
|
@param service(jid.JID, None): jid of PubSub service
|
|
None to use our PEP
|
|
@param nodeIdentifier(unicode): node to get schema from
|
|
@return (domish.Element, None): schema (<x> element)
|
|
None if not schema has been set on this node
|
|
"""
|
|
iq_elt = client.IQ(u'get')
|
|
if service is not None:
|
|
iq_elt['to'] = service.full()
|
|
pubsub_elt = iq_elt.addElement((NS_SCHEMA, 'pubsub'))
|
|
schema_elt = pubsub_elt.addElement((NS_SCHEMA, 'schema'))
|
|
schema_elt['node'] = nodeIdentifier
|
|
d = iq_elt.send()
|
|
d.addCallback(self._getSchemaCb)
|
|
return d
|
|
|
|
@defer.inlineCallbacks
|
|
def getSchemaForm(self, client, service, nodeIdentifier, schema=None, form_type='form', copy_form=True):
|
|
"""get data form from node's schema
|
|
|
|
@param service(None, jid.JID): PubSub service
|
|
@param nodeIdentifier(unicode): node
|
|
@param schema(domish.Element, data_form.Form, None): node schema
|
|
if domish.Element, will be converted to data form
|
|
if data_form.Form it will be returned without modification
|
|
if None, it will be retrieved from node (imply one additional XMPP request)
|
|
@param form_type(unicode): type of the form
|
|
@param copy_form(bool): if True and if schema is already a data_form.Form, will deep copy it before returning
|
|
needed when the form is reused and it will be modified (e.g. in sendDataFormItem)
|
|
@return(data_form.Form): data form
|
|
the form should not be modified if copy_form is not set
|
|
"""
|
|
if schema is None:
|
|
log.debug(_(u"unspecified schema, we need to request it"))
|
|
schema = yield self.getSchema(client, service, nodeIdentifier)
|
|
if schema is None:
|
|
raise exceptions.DataError(_(u"no schema specified, and this node has no schema either, we can't construct the data form"))
|
|
elif isinstance(schema, data_form.Form):
|
|
if copy_form:
|
|
schema = copy.deepcopy(schema)
|
|
defer.returnValue(schema)
|
|
|
|
try:
|
|
form = data_form.Form.fromElement(schema)
|
|
except data_form.Error as e:
|
|
raise exceptions.DataError(_(u"Invalid Schema: {msg}").format(
|
|
msg = e))
|
|
form.formType = form_type
|
|
defer.returnValue(form)
|
|
|
|
def schema2XMLUI(self, schema_elt):
|
|
form = data_form.Form.fromElement(schema_elt)
|
|
xmlui = xml_tools.dataForm2XMLUI(form, '')
|
|
return xmlui
|
|
|
|
def _getUISchema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE):
|
|
client = self.host.getClient(profile_key)
|
|
service = None if not service else jid.JID(service)
|
|
d = self.getUISchema(client, service, nodeIdentifier)
|
|
d.addCallback(lambda xmlui: xmlui.toXml())
|
|
return d
|
|
|
|
def getUISchema(self, client, service, nodeIdentifier):
|
|
d = self.getSchema(client, service, nodeIdentifier)
|
|
d.addCallback(self.schema2XMLUI)
|
|
return d
|
|
|
|
def _setSchema(self, service, nodeIdentifier, schema, profile_key=C.PROF_KEY_NONE):
|
|
client = self.host.getClient(profile_key)
|
|
service = None if not service else jid.JID(service)
|
|
schema = generic.parseXml(schema.encode('utf-8'))
|
|
return self.setSchema(client, service, nodeIdentifier, schema)
|
|
|
|
def setSchema(self, client, service, nodeIdentifier, schema):
|
|
"""set or replace PubSub node schema
|
|
|
|
@param schema(domish.Element, None): schema to set
|
|
None if schema need to be removed
|
|
"""
|
|
iq_elt = client.IQ()
|
|
if service is not None:
|
|
iq_elt['to'] = service.full()
|
|
pubsub_elt = iq_elt.addElement((NS_SCHEMA, 'pubsub'))
|
|
schema_elt = pubsub_elt.addElement((NS_SCHEMA, 'schema'))
|
|
schema_elt['node'] = nodeIdentifier
|
|
if schema is not None:
|
|
schema_elt.addChild(schema)
|
|
return iq_elt.send()
|
|
|
|
def _getDataFormItems(self, form_ns='', service='', node='', schema='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, profile_key=C.PROF_KEY_NONE):
|
|
client = self.host.getClient(profile_key)
|
|
service = jid.JID(service) if service else None
|
|
if not node:
|
|
raise exceptions.DataError(_(u'empty node is not allowed'))
|
|
if schema:
|
|
schema = generic.parseXml(schema.encode('utf-8'))
|
|
else:
|
|
schema = None
|
|
max_items = None if max_items == C.NO_LIMIT else max_items
|
|
extra = self._p.parseExtra(extra_dict)
|
|
d = self.getDataFormItems(client, form_ns or None, service, node, schema, max_items or None, item_ids, sub_id or None, extra.rsm_request, extra.extra)
|
|
d.addCallback(self._p.serItemsData)
|
|
return d
|
|
|
|
@defer.inlineCallbacks
|
|
def getDataFormItems(self, client, form_ns, service, nodeIdentifier, schema=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None, filters=None):
|
|
"""Get items known as being data forms, and convert them to XMLUI
|
|
|
|
@param form_ns (unicode, None): namespace of the form
|
|
None to accept everything, even if form has no namespace
|
|
@param schema(domish.Element, data_form.Form, None): schema of the node if known
|
|
if None, it will be retrieved from node
|
|
@param filters(dict, None): same as for xml_tools.dataFormResult2XMLUI
|
|
other parameters as the same as for [getItems]
|
|
@return (list[unicode]): XMLUI of the forms
|
|
if an item is invalid (not corresponding to form_ns or not a data_form)
|
|
it will be skipped
|
|
"""
|
|
# we need the initial form to get options of fields when suitable
|
|
schema_form = yield self.getSchemaForm(client, service, nodeIdentifier, schema, form_type='result', copy_form=False)
|
|
items_data = yield self._p.getItems(client, service, nodeIdentifier, max_items, item_ids, sub_id, rsm_request, extra)
|
|
items, metadata = items_data
|
|
items_xmlui = []
|
|
for item_elt in items:
|
|
for x_elt in item_elt.elements((data_form.NS_X_DATA, u'x')):
|
|
form = data_form.Form.fromElement(x_elt)
|
|
if form_ns and form.formNamespace != form_ns:
|
|
continue
|
|
xmlui = xml_tools.dataFormResult2XMLUI(
|
|
form,
|
|
schema_form,
|
|
prepend = (('label', 'id'),('text', item_elt['id'], u'id')),
|
|
filters = filters,
|
|
)
|
|
items_xmlui.append(xmlui)
|
|
break
|
|
defer.returnValue((items_xmlui, metadata))
|
|
|
|
|
|
def _sendDataFormItem(self, service, nodeIdentifier, values, schema=None, item_id=None, extra=None, profile_key=C.PROF_KEY_NONE):
|
|
client = self.host.getClient(profile_key)
|
|
service = None if not service else jid.JID(service)
|
|
if schema:
|
|
schema = generic.parseXml(schema.encode('utf-8'))
|
|
else:
|
|
schema = None
|
|
d = self.sendDataFormItem(client, service, nodeIdentifier, values, schema, item_id or None, extra, deserialise=True)
|
|
d.addCallback(lambda ret: ret or u'')
|
|
return d
|
|
|
|
@defer.inlineCallbacks
|
|
def sendDataFormItem(self, client, service, nodeIdentifier, values, schema=None, item_id=None, extra=None, deserialise=False):
|
|
"""Publish an item as a dataform when we know that there is a schema
|
|
|
|
@param values(dict[[iterable[object], object]): values set for the form
|
|
if not iterable, will be put in a list
|
|
@param schema(domish.Element, data_form.Form, None): data schema
|
|
None to retrieve data schema from node (need to do a additional XMPP call)
|
|
Schema is needed to construct data form to publish
|
|
@param deserialise(bool): if True, data are list of unicode and must be deserialized according to expected type
|
|
This is done in this method and not directly in _sendDataFormItem because we need to know the data type
|
|
which is in the form, not availablable in _sendDataFormItem
|
|
other parameters as the same as for [self._p.sendItem]
|
|
"""
|
|
form = yield self.getSchemaForm(client, service, nodeIdentifier, schema, form_type='submit')
|
|
|
|
for name, values_list in values.iteritems():
|
|
try:
|
|
field = form.fields[name]
|
|
except KeyError:
|
|
log.warning(_(u"field {name} doesn't exist, ignoring it").format(name=name))
|
|
continue
|
|
if isinstance(values_list, basestring) or not isinstance(values_list, Iterable):
|
|
values_list = [values_list]
|
|
if deserialise:
|
|
if field.fieldType == 'boolean':
|
|
values_list = [C.bool(v) for v in values_list]
|
|
elif 'jid' in field.fieldType:
|
|
values_list = [jid.JID(v) for v in values_list]
|
|
if 'list' in field.fieldType:
|
|
# for lists, we check that given values are allowed in form
|
|
allowed_values = [o.value for o in field.options]
|
|
values_list = [v for v in values_list if v in allowed_values]
|
|
if not values_list:
|
|
# if values don't map to allowed values, we use default ones
|
|
values_list = field.values
|
|
field.values = values_list
|
|
|
|
yield self._p.sendItem(client, service, nodeIdentifier, form.toElement(), item_id, extra)
|
|
|
|
|
|
class SchemaHandler(XMPPHandler):
|
|
implements(iwokkel.IDisco)
|
|
|
|
def getDiscoInfo(self, requestor, service, nodeIdentifier=''):
|
|
return [disco.DiscoFeature(NS_SCHEMA)]
|
|
|
|
def getDiscoItems(self, requestor, service, nodeIdentifier=''):
|
|
return []
|