#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2019 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 twisted.words.protocols.jabber import jid from twisted.internet import defer from sat.core import exceptions from collections import OrderedDict import html.entities import re """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 html_entity_re = re.compile(r"&([a-zA-Z]+?);") XML_ENTITIES = ("quot", "amp", "apos", "lt", "gt") # method to clean XHTML, receive raw unsecure XML or HTML, must return cleaned raw XHTML # this method must be set during runtime cleanXHTML = None # TODO: move XMLUI stuff in a separate module # TODO: rewrite this with lxml or ElementTree or domish.Element: it's complicated and difficult to maintain with current minidom implementation # 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 is None and field.ext_type is not None: # we have an extended field if field.ext_type == "xml": element = field.value if element.uri == C.NS_XHTML: widget_type = "xhtmlbox" widget_args[0] = element.toXml() widget_kwargs["read_only"] = read_only else: log.warning("unknown XML element, falling back to textbox") widget_type = "textbox" widget_args[0] = element.toXml() widget_kwargs["read_only"] = read_only else: raise exceptions.DataError("unknown extended type {ext_type}".format( ext_type = field.ext_type)) elif 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] = "\n".join(field.values) widget_kwargs["read_only"] = read_only elif field.fieldType == "hidden": widget_type = "hidden" 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( "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, prepend=None, filters=None): """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 @param prepend(iterable, None): widgets to prepend to main LabelContainer if not None, must be an iterable of *args for addWidget. Those widgets will be added first to the container. @param filters(dict, None): if not None, a dictionary of callable: key is the name of the widget to filter the value is a callable, it will get form's XMLUI, widget's type, args and kwargs and must return widget's type, args and kwargs (which can be modified) This is especially useful to modify well known fields @return: the completed XMLUI instance """ if filters is None: filters = {} if form.instructions: form_ui.addText("\n".join(form.instructions), "instructions") form_ui.changeContainer("label") if prepend is not None: for widget_args in prepend: form_ui.addWidget(*widget_args) for field in form.fieldList: widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData( field, read_only ) try: widget_filter = filters[widget_kwargs["name"]] except KeyError: pass else: widget_type, widget_args, widget_kwargs = widget_filter( form_ui, widget_type, widget_args, widget_kwargs ) if widget_type != "hidden": 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 (Wokkel's XEP-0004 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 dataFormEltResult2XMLUIData(form_xml): """Parse a data form result (not parsed by Wokkel's XEP-0004 implementation). The raw data form is used because Wokkel doesn't manage result items parsing yet. @param form_xml (domish.Element): element of the data form @return: a couple (headers, result_list): - headers (dict{unicode: unicode}): form headers (field labels and types) - xmlui_data (list[tuple]): list of (widget_type, widget_args, widget_kwargs) """ headers = OrderedDict() try: reported_elt = next(form_xml.elements("jabber:x:data", "reported")) except StopIteration: raise exceptions.DataError( "Couldn't find expected tag in %s" % form_xml.toXml() ) 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)") xmlui_data = [] 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("Unexpected tag (%s)" % elt.name) continue field = data_form.Field.fromElement(elt) xmlui_data.append(_dataFormField2XMLUIData(field)) return headers, xmlui_data def XMLUIData2AdvancedList(xmlui, headers, xmlui_data): """Take a raw data form result (not parsed by Wokkel's XEP-0004 implementation) 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 headers (dict{unicode: unicode}): form headers (field labels and types) @param xmlui_data (list[tuple]): list of (widget_type, widget_args, widget_kwargs) @return: the completed XMLUI instance """ adv_list = AdvancedListContainer( xmlui, headers=headers, columns=len(headers), parent=xmlui.current_container ) xmlui.changeContainer(adv_list) for widget_type, widget_args, widget_kwargs in xmlui_data: xmlui.addWidget(widget_type, *widget_args, **widget_kwargs) return xmlui def dataFormResult2AdvancedList(xmlui, form_xml): """Take a raw data form result (not parsed by Wokkel's XEP-0004 implementation) 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: the completed XMLUI instance """ headers, xmlui_data = dataFormEltResult2XMLUIData(form_xml) XMLUIData2AdvancedList(xmlui, headers, xmlui_data) def dataFormEltResult2XMLUI(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 dataFormResult2XMLUI(result_form, base_form, session_id=None, prepend=None, filters=None, read_only=True): """Convert data form result to SàT XMLUI. @param result_form (data_form.Form): result form to convert @param base_form (data_form.Form): initial form (i.e. of form type "form") this one is necessary to reconstruct options when needed (e.g. list elements) @param session_id (unicode): session id to return with the data @param prepend: same as for [dataForm2Widgets] @param filters: same as for [dataForm2Widgets] @param read_only: same as for [dataForm2Widgets] @return: XMLUI instance """ # we deepcopy the form because _dataFormField2XMLUIData can modify the value # FIXME: check if it's really important, the only modified value seems to be # the replacement of None by "" on fixed fields # form = deepcopy(result_form) form = result_form for name, field in form.fields.items(): try: base_field = base_form.fields[name] except KeyError: continue field.options = base_field.options[:] xml_ui = XMLUI("window", "vertical", session_id=session_id) dataForm2Widgets(xml_ui, form, read_only=read_only, prepend=prepend, filters=filters) 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, str): return str(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 """ ret = {} for key, value in xmlui_data.items(): if not key.startswith(SAT_FORM_PREFIX): continue if isinstance(value, str) and '\n' in value: # data form expects multi-lines text to be in separated values value = value.split('\n') ret[key[len(SAT_FORM_PREFIX) :]] = _cleanValue(value) return ret def formEscape(name): """Return escaped name for forms. @param name (unicode): form name @return: unicode """ return "%s%s" % (SAT_FORM_PREFIX, name) def isXMLUICancelled(raw_xmlui): """Tell if an XMLUI has been cancelled by checking raw XML""" return C.bool(raw_xmlui.get('cancelled', C.BOOL_FALSE)) 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 """ # TODO: refactor params and use Twisted directly to parse XML 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=LabelContainer) 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 = _paramsGetListOptions(param) widget_kwargs["options"] = options widget_kwargs["selected"] = selected widget_kwargs["styles"] = ["extensible"] elif type_ == "jids_list": widget_kwargs["jids"] = _paramsGetListJids(param) 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 _paramsGetListOptions(param): """Retrieve the options for list element. The