#!/usr/bin/python # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 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 . from sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from xml.dom import minidom, NotFoundErr from wokkel import data_form from twisted.words.xish import domish from sat.core import exceptions """This library help manage XML used in SàT (parameters, registration, etc)""" SAT_FORM_PREFIX = "SAT_FORM_" SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names # Helper functions def _dataFormField2XMLUIData(field, read_only=False): """Get data needed to create an XMLUI's Widget from Wokkel's data_form's Field. The attribute field can be modified (if it's fixed and it has no value). @param field (data_form.Field): a field with attributes "value", "fieldType", "label" and "var" @param read_only (bool): if True and it makes sense, create a read only input widget @return: a tuple (widget_type, widget_args, widget_kwargs) """ widget_args = [field.value] widget_kwargs = {} if field.fieldType == 'fixed' or field.fieldType is None: widget_type = 'text' if field.value is None: if field.label is None: log.warning(_("Fixed field has neither value nor label, ignoring it")) field.value = "" else: field.value = field.label field.label = None widget_args[0] = field.value elif field.fieldType == 'text-single': widget_type = "string" widget_kwargs['read_only'] = read_only elif field.fieldType == 'jid-single': widget_type = "jid_input" widget_kwargs['read_only'] = read_only elif field.fieldType == 'text-multi': widget_type = "textbox" widget_args[0] = u'\n'.join(field.values) widget_kwargs['read_only'] = read_only elif field.fieldType == 'text-private': widget_type = "password" widget_kwargs['read_only'] = read_only elif field.fieldType == 'boolean': widget_type = "bool" if widget_args[0] is None: widget_args[0] = 'false' widget_kwargs['read_only'] = read_only elif field.fieldType == 'integer': widget_type = "integer" widget_kwargs['read_only'] = read_only elif field.fieldType == 'list-single': widget_type = "list" widget_kwargs["options"] = [(option.value, option.label or option.value) for option in field.options] widget_kwargs["selected"] = widget_args widget_args = [] else: log.error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType) widget_type = "string" widget_kwargs['read_only'] = read_only if field.var: widget_kwargs["name"] = field.var return widget_type, widget_args, widget_kwargs def dataForm2Widgets(form_ui, form, read_only=False): """Complete an existing XMLUI with widget converted from XEP-0004 data forms. @param form_ui (XMLUI): XMLUI instance @param form (data_form.Form): Wokkel's implementation of data form @param read_only (bool): if True and it makes sense, create a read only input widget @return: the completed XMLUI instance """ if form.instructions: form_ui.addText('\n'.join(form.instructions), 'instructions') form_ui.changeContainer("pairs") for field in form.fieldList: widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field, read_only) label = field.label or field.var if label: form_ui.addLabel(label) else: form_ui.addEmpty() form_ui.addWidget(widget_type, *widget_args, **widget_kwargs) return form_ui def dataForm2XMLUI(form, submit_id, session_id=None, read_only=False): """Take a data form (XEP-0004, Wokkel's implementation) and convert it to a SàT XMLUI. @param form (data_form.Form): a Form instance @param submit_id (unicode): callback id to call when submitting form @param session_id (unicode): session id to return with the data @param read_only (bool): if True and it makes sense, create a read only input widget @return: XMLUI instance """ form_ui = XMLUI("form", "vertical", submit_id=submit_id, session_id=session_id) return dataForm2Widgets(form_ui, form, read_only=read_only) def dataFormResult2AdvancedList(xmlui, form_xml): """Take a raw data form (not parsed by XEP-0004) and convert it to an advanced list. The raw data form is used because Wokkel doesn't manage result items parsing yet. @param xmlui (XMLUI): the XMLUI where the AdvancedList will be added @param form_xml (domish.Element): element of the data form @return: AdvancedList element """ headers = {} try: reported_elt = form_xml.elements('jabber:x:data', 'reported').next() except StopIteration: raise exceptions.DataError("Couldn't find expected tag") for elt in reported_elt.elements(): if elt.name != "field": raise exceptions.DataError("Unexpected tag") name = elt["var"] label = elt.attributes.get('label', '') type_ = elt.attributes.get('type') headers[name] = (label, type_) if not headers: raise exceptions.DataError("No reported fields (see XEP-0004 §3.4)") adv_list = AdvancedListContainer(xmlui, headers=headers, columns=len(headers), parent=xmlui.current_container) xmlui.changeContainer(adv_list) item_elts = form_xml.elements('jabber:x:data', 'item') for item_elt in item_elts: for elt in item_elt.elements(): if elt.name != 'field': log.warning(u"Unexpected tag (%s)" % elt.name) continue field = data_form.Field.fromElement(elt) widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field) xmlui.addWidget(widget_type, *widget_args, **widget_kwargs) return xmlui def dataFormResult2XMLUI(form_elt, session_id=None): """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI. The raw data form is used because Wokkel doesn't manage result items parsing yet. @param form_elt (domish.Element): element of the data form @param session_id (unicode): session id to return with the data @return: XMLUI instance """ xml_ui = XMLUI("window", "vertical", session_id=session_id) try: dataFormResult2AdvancedList(xml_ui, form_elt) except exceptions.DataError: parsed_form = data_form.Form.fromElement(form_elt) dataForm2Widgets(xml_ui, parsed_form, read_only=True) return xml_ui def _cleanValue(value): """Workaround method to avoid DBus types with D-Bus bridge. @param value: value to clean @return: value in a non DBus type (only clean string yet) """ # XXX: must be removed when DBus types will no cause problems anymore # FIXME: should be cleaned inside D-Bus bridge itself if isinstance(value, basestring): return unicode(value) return value def XMLUIResult2DataFormResult(xmlui_data): """ Extract form data from a XMLUI return. @param xmlui_data (dict): data returned by frontends for XMLUI form @return: dict of data usable by Wokkel's data form """ return {key[len(SAT_FORM_PREFIX):]: _cleanValue(value) for key, value in xmlui_data.iteritems() if key.startswith(SAT_FORM_PREFIX)} def formEscape(name): """Return escaped name for forms. @param name (unicode): form name @return: unicode """ return u"%s%s" % (SAT_FORM_PREFIX, name) def XMLUIResultToElt(xmlui_data): """Construct result domish.Element from XMLUI result. @param xmlui_data (dict): data returned by frontends for XMLUI form @return: domish.Element """ form = data_form.Form('submit') form.makeFields(XMLUIResult2DataFormResult(xmlui_data)) return form.toElement() def tupleList2dataForm(values): """Convert a list of tuples (name, value) to a wokkel submit data form. @param values (list): list of tuples @return: data_form.Form """ form = data_form.Form('submit') for value in values: field = data_form.Field(var=value[0], value=value[1]) form.addField(field) return form def paramsXML2XMLUI(xml): """Convert the XML for parameter to a SàT XML User Interface. @param xml (unicode) @return: XMLUI """ params_doc = minidom.parseString(xml.encode('utf-8')) top = params_doc.documentElement if top.nodeName != 'params': raise exceptions.DataError(_('INTERNAL ERROR: parameters xml not valid')) param_ui = XMLUI("param", "tabs") tabs_cont = param_ui.current_container for category in top.getElementsByTagName("category"): category_name = category.getAttribute('name') label = category.getAttribute('label') if not category_name: raise exceptions.DataError(_('INTERNAL ERROR: params categories must have a name')) tabs_cont.addTab(category_name, label=label, container=PairsContainer) for param in category.getElementsByTagName("param"): widget_kwargs = {} param_name = param.getAttribute('name') param_label = param.getAttribute('label') type_ = param.getAttribute('type') if not param_name and type_ != 'text': raise exceptions.DataError(_('INTERNAL ERROR: params must have a name')) value = param.getAttribute('value') or None callback_id = param.getAttribute('callback_id') or None if type_ == 'list': options, selected = _getParamListOptions(param) widget_kwargs['options'] = options widget_kwargs['selected'] = selected if type_ in ("button", "text"): param_ui.addEmpty() value = param_label else: param_ui.addLabel(param_label or param_name) if value: widget_kwargs["value"] = value if callback_id: widget_kwargs['callback_id'] = callback_id others = ["%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, other.getAttribute('name')) for other in category.getElementsByTagName('param') if other.getAttribute('type') != 'button'] widget_kwargs['fields_back'] = others widget_kwargs['name'] = "%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name) param_ui.addWidget(type_, **widget_kwargs) return param_ui.toXml() def _getParamListOptions(param): """Retrieve the options for list element. The