diff libervia/backend/tools/xml_tools.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/tools/xml_tools.py@2594e1951cf7
children b274f0d5c138
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/backend/tools/xml_tools.py	Fri Jun 02 11:49:51 2023 +0200
@@ -0,0 +1,2093 @@
+#!/usr/bin/env python3
+
+# SAT: a jabber client
+# Copyright (C) 2009-2021 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 collections import OrderedDict
+import html.entities
+import re
+from typing import Dict, Optional, Tuple, Union, Literal, overload, Iterable
+from xml.dom import NotFoundErr, minidom
+import xml.etree.ElementTree as ET
+from lxml import etree
+
+from twisted.internet import defer
+from twisted.words.protocols.jabber import jid
+from twisted.words.xish import domish
+from wokkel import data_form
+
+from libervia.backend.core import exceptions
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core.i18n import _
+from libervia.backend.core.log import getLogger
+
+
+log = getLogger(__name__)
+
+"""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
+clean_xhtml = 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 _data_form_field_2_xmlui_data(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.values or [None]
+    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 = [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 = ["\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 = ["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 = []
+    elif field.fieldType == "list-multi":
+        widget_type = "list"
+        widget_kwargs["options"] = [
+            (option.value, option.label or option.value) for option in field.options
+        ]
+        widget_kwargs["selected"] = widget_args
+        widget_kwargs["styles"] =  ["multi"]
+        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 data_form_2_widgets(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 add_widget. 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.change_container("label")
+
+    if prepend is not None:
+        for widget_args in prepend:
+            form_ui.add_widget(*widget_args)
+
+    for field in form.fieldList:
+        widget_type, widget_args, widget_kwargs = _data_form_field_2_xmlui_data(
+            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.add_widget(widget_type, *widget_args, **widget_kwargs)
+
+    return form_ui
+
+
+def data_form_2_xmlui(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 data_form_2_widgets(form_ui, form, read_only=read_only)
+
+
+def data_form_2_data_dict(form: data_form.Form) -> dict:
+    """Convert data form to a simple dict, easily serialisable
+
+    see data_dict_2_data_form for a description of the format
+    """
+    fields = []
+    data_dict = {
+        "fields": fields
+    }
+    if form.formNamespace:
+        data_dict["namespace"] = form.formNamespace
+    for form_field in form.fieldList:
+        field = {"type": form_field.fieldType}
+        fields.append(field)
+        for src_name, dest_name in (
+            ('var', 'name'),
+            ('label', 'label'),
+            ('value', 'value'),
+            # FIXME: we probably should have only "values"
+            ('values', 'values')
+        ):
+            value = getattr(form_field, src_name, None)
+            if value:
+                field[dest_name] = value
+        if form_field.options:
+            options = field["options"] = []
+            for form_opt in form_field.options:
+                opt = {"value": form_opt.value}
+                if form_opt.label:
+                    opt["label"] = form_opt.label
+                options.append(opt)
+
+        if form_field.fieldType is None and form_field.ext_type == "xml":
+            if isinstance(form_field.value, domish.Element):
+                if ((form_field.value.uri == C.NS_XHTML
+                     and form_field.value.name == "div")):
+                    field["type"] = "xhtml"
+                    if form_field.value.children:
+                        log.warning(
+                            "children are not managed for XHTML fields: "
+                            f"{form_field.value.toXml()}"
+                        )
+    return data_dict
+
+
+def data_dict_2_data_form(data_dict):
+    """Convert serialisable dict of data to a data form
+
+    The format of the dict is as follow:
+        - an optional "namespace" key with form namespace
+        - a mandatory "fields" key with list of fields as follow:
+            - "type" is mostly the same as data_form.Field.fieldType
+            - "name" is used to set the "var" attribute of data_form.Field
+            - "label", and "value" follow same attribude in data_form.Field
+            - "xhtml" is used for "xml" fields with child in the C.NS_XHTML namespace
+            - "options" are list of dict with optional "label" and mandatory "value"
+              following suitable attributes from data_form.Option
+            - "required" is the same as data_form.Field.required
+    """
+    # TODO: describe format
+    fields = []
+    for field_data in data_dict["fields"]:
+        field_type = field_data.get('type', 'text-single')
+        kwargs = {
+            "fieldType": field_type,
+            "var": field_data["name"],
+            "label": field_data.get('label'),
+            "value": field_data.get("value"),
+            "required": field_data.get("required")
+        }
+        if field_type == "xhtml":
+            kwargs.update({
+                "fieldType": None,
+                "ext_type": "xml",
+            })
+            if kwargs["value"] is None:
+                kwargs["value"] = domish.Element((C.NS_XHTML, "div"))
+        elif "options" in field_data:
+            kwargs["options"] = [
+                data_form.Option(o["value"], o.get("label"))
+                for o in field_data["options"]
+            ]
+        field = data_form.Field(**kwargs)
+        fields.append(field)
+    return data_form.Form(
+        "form",
+        formNamespace=data_dict.get("namespace"),
+        fields=fields
+    )
+
+
+def data_form_elt_result_2_xmlui_data(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 <reported> 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(_data_form_field_2_xmlui_data(field))
+
+    return headers, xmlui_data
+
+
+def xmlui_data_2_advanced_list(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.change_container(adv_list)
+
+    for widget_type, widget_args, widget_kwargs in xmlui_data:
+        xmlui.add_widget(widget_type, *widget_args, **widget_kwargs)
+
+    return xmlui
+
+
+def data_form_result_2_advanced_list(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 = data_form_elt_result_2_xmlui_data(form_xml)
+    xmlui_data_2_advanced_list(xmlui, headers, xmlui_data)
+
+
+def data_form_elt_result_2_xmlui(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:
+        data_form_result_2_advanced_list(xml_ui, form_elt)
+    except exceptions.DataError:
+        parsed_form = data_form.Form.fromElement(form_elt)
+        data_form_2_widgets(xml_ui, parsed_form, read_only=True)
+    return xml_ui
+
+
+def data_form_result_2_xmlui(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 [data_form_2_widgets]
+    @param filters: same as for [data_form_2_widgets]
+    @param read_only: same as for [data_form_2_widgets]
+    @return: XMLUI instance
+    """
+    # we deepcopy the form because _data_form_field_2_xmlui_data 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)
+    data_form_2_widgets(xml_ui, form, read_only=read_only, prepend=prepend, filters=filters)
+    return xml_ui
+
+
+def _clean_value(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 xmlui_result_2_data_form_result(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):
+            if "\n" in value:
+                # data form expects multi-lines text to be in separated values
+                value = value.split('\n')
+            elif "\t" in value:
+                # FIXME: workaround to handle multiple values. Proper serialisation must
+                #   be done in XMLUI
+                value = value.split("\t")
+        ret[key[len(SAT_FORM_PREFIX) :]] = _clean_value(value)
+    return ret
+
+
+def form_escape(name):
+    """Return escaped name for forms.
+
+    @param name (unicode): form name
+    @return: unicode
+    """
+    return "%s%s" % (SAT_FORM_PREFIX, name)
+
+
+def is_xmlui_cancelled(raw_xmlui):
+    """Tell if an XMLUI has been cancelled by checking raw XML"""
+    return C.bool(raw_xmlui.get('cancelled', C.BOOL_FALSE))
+
+
+def xmlui_result_to_elt(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(xmlui_result_2_data_form_result(xmlui_data))
+    return form.toElement()
+
+
+def tuple_list_2_data_form(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 params_xml_2_xmlui(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.add_tab(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 = _params_get_list_options(param)
+                widget_kwargs["options"] = options
+                widget_kwargs["selected"] = selected
+                widget_kwargs["styles"] = ["extensible"]
+            elif type_ == "jids_list":
+                widget_kwargs["jids"] = _params_get_list_jids(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.add_widget(type_, **widget_kwargs)
+
+    return param_ui.toXml()
+
+
+def _params_get_list_options(param):
+    """Retrieve the options for list element.
+
+    The <option/> tags must be direct children of <param/>.
+    @param param (domish.Element): element
+    @return: a tuple (options, selected_value)
+    """
+    if len(param.getElementsByTagName("options")) > 0:
+        raise exceptions.DataError(
+            _("The 'options' tag is not allowed in parameter of type 'list'!")
+        )
+    elems = param.getElementsByTagName("option")
+    if len(elems) == 0:
+        return []
+    options = []
+    for elem in elems:
+        value = elem.getAttribute("value")
+        if not value:
+            raise exceptions.InternalError("list option must have a value")
+        label = elem.getAttribute("label")
+        if label:
+            options.append((value, label))
+        else:
+            options.append(value)
+    selected = [
+        elem.getAttribute("value")
+        for elem in elems
+        if elem.getAttribute("selected") == "true"
+    ]
+    return (options, selected)
+
+
+def _params_get_list_jids(param):
+    """Retrive jids from a jids_list element.
+
+    the <jid/> tags must be direct children of <param/>
+    @param param (domish.Element): element
+    @return: a list of jids
+    """
+    elems = param.getElementsByTagName("jid")
+    jids = [
+        elem.firstChild.data
+        for elem in elems
+        if elem.firstChild is not None and elem.firstChild.nodeType == elem.TEXT_NODE
+    ]
+    return jids
+
+
+### XMLUI Elements ###
+
+
+class Element(object):
+    """ Base XMLUI element """
+
+    type = None
+
+    def __init__(self, xmlui, parent=None):
+        """Create a container element
+
+        @param xmlui: XMLUI instance
+        @parent: parent element
+        """
+        assert self.type is not None
+        self.children = []
+        if not hasattr(self, "elem"):
+            self.elem = parent.xmlui.doc.createElement(self.type)
+        self.xmlui = xmlui
+        if parent is not None:
+            parent.append(self)
+        self.parent = parent
+
+    def append(self, child):
+        """Append a child to this element.
+
+        @param child (Element): child element
+        @return: the added child Element
+        """
+        self.elem.appendChild(child.elem)
+        child.parent = self
+        self.children.append(child)
+        return child
+
+
+class TopElement(Element):
+    """ Main XML Element """
+
+    type = "top"
+
+    def __init__(self, xmlui):
+        self.elem = xmlui.doc.documentElement
+        super(TopElement, self).__init__(xmlui)
+
+
+class TabElement(Element):
+    """ Used by TabsContainer to give name and label to tabs."""
+
+    type = "tab"
+
+    def __init__(self, parent, name, label, selected=False):
+        """
+
+        @param parent (TabsContainer): parent container
+        @param name (unicode): tab name
+        @param label (unicode): tab label
+        @param selected (bool): set to True to select this tab
+        """
+        if not isinstance(parent, TabsContainer):
+            raise exceptions.DataError(_("TabElement must be a child of TabsContainer"))
+        super(TabElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute("name", name)
+        self.elem.setAttribute("label", label)
+        if selected:
+            self.set_selected(selected)
+
+    def set_selected(self, selected=False):
+        """Set the tab selected.
+
+        @param selected (bool): set to True to select this tab
+        """
+        self.elem.setAttribute("selected", "true" if selected else "false")
+
+
+class FieldBackElement(Element):
+    """ Used by ButtonWidget to indicate which field have to be sent back """
+
+    type = "field_back"
+
+    def __init__(self, parent, name):
+        assert isinstance(parent, ButtonWidget)
+        super(FieldBackElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute("name", name)
+
+
+class InternalFieldElement(Element):
+    """ Used by internal callbacks to indicate which fields are manipulated """
+
+    type = "internal_field"
+
+    def __init__(self, parent, name):
+        super(InternalFieldElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute("name", name)
+
+
+class InternalDataElement(Element):
+    """ Used by internal callbacks to retrieve extra data """
+
+    type = "internal_data"
+
+    def __init__(self, parent, children):
+        super(InternalDataElement, self).__init__(parent.xmlui, parent)
+        assert isinstance(children, list)
+        for child in children:
+            self.elem.childNodes.append(child)
+
+
+class OptionElement(Element):
+    """" Used by ListWidget to specify options """
+
+    type = "option"
+
+    def __init__(self, parent, option, selected=False):
+        """
+
+        @param parent
+        @param option (string, tuple)
+        @param selected (boolean)
+        """
+        assert isinstance(parent, ListWidget)
+        super(OptionElement, self).__init__(parent.xmlui, parent)
+        if isinstance(option, str):
+            value, label = option, option
+        elif isinstance(option, tuple):
+            value, label = option
+        else:
+            raise NotImplementedError
+        self.elem.setAttribute("value", value)
+        self.elem.setAttribute("label", label)
+        if selected:
+            self.elem.setAttribute("selected", "true")
+
+
+class JidElement(Element):
+    """" Used by JidsListWidget to specify jids"""
+
+    type = "jid"
+
+    def __init__(self, parent, jid_):
+        """
+        @param jid_(jid.JID, unicode): jid to append
+        """
+        assert isinstance(parent, JidsListWidget)
+        super(JidElement, self).__init__(parent.xmlui, parent)
+        if isinstance(jid_, jid.JID):
+            value = jid_.full()
+        elif isinstance(jid_, str):
+            value = str(jid_)
+        else:
+            raise NotImplementedError
+        jid_txt = self.xmlui.doc.createTextNode(value)
+        self.elem.appendChild(jid_txt)
+
+
+class RowElement(Element):
+    """" Used by AdvancedListContainer """
+
+    type = "row"
+
+    def __init__(self, parent):
+        assert isinstance(parent, AdvancedListContainer)
+        super(RowElement, self).__init__(parent.xmlui, parent)
+        if parent.next_row_idx is not None:
+            if parent.auto_index:
+                raise exceptions.DataError(_("Can't set row index if auto_index is True"))
+            self.elem.setAttribute("index", parent.next_row_idx)
+            parent.next_row_idx = None
+
+
+class HeaderElement(Element):
+    """" Used by AdvancedListContainer """
+
+    type = "header"
+
+    def __init__(self, parent, name=None, label=None, description=None):
+        """
+        @param parent: AdvancedListContainer instance
+        @param name: name of the container
+        @param label: label to be displayed in columns
+        @param description: long descriptive text
+        """
+        assert isinstance(parent, AdvancedListContainer)
+        super(HeaderElement, self).__init__(parent.xmlui, parent)
+        if name:
+            self.elem.setAttribute("name", name)
+        if label:
+            self.elem.setAttribute("label", label)
+        if description:
+            self.elem.setAttribute("description", description)
+
+
+## Containers ##
+
+
+class Container(Element):
+    """ And Element which contains other ones and has a layout """
+
+    type = None
+
+    def __init__(self, xmlui, parent=None):
+        """Create a container element
+
+        @param xmlui: XMLUI instance
+        @parent: parent element or None
+        """
+        self.elem = xmlui.doc.createElement("container")
+        super(Container, self).__init__(xmlui, parent)
+        self.elem.setAttribute("type", self.type)
+
+    def get_parent_container(self):
+        """ Return first parent container
+
+        @return: parent container or None
+        """
+        current = self.parent
+        while not isinstance(current, (Container)) and current is not None:
+            current = current.parent
+        return current
+
+
+class VerticalContainer(Container):
+    type = "vertical"
+
+
+class HorizontalContainer(Container):
+    type = "horizontal"
+
+
+class PairsContainer(Container):
+    """Container with series of 2 elements"""
+    type = "pairs"
+
+
+class LabelContainer(Container):
+    """Like PairsContainer, but first element can only be a label"""
+    type = "label"
+
+
+class TabsContainer(Container):
+    type = "tabs"
+
+    def add_tab(self, name, label=None, selected=None, container=VerticalContainer):
+        """Add a tab.
+
+        @param name (unicode): tab name
+        @param label (unicode): tab label
+        @param selected (bool): set to True to select this tab
+        @param container (class): container class, inheriting from Container
+        @return: the container for the new tab
+        """
+        if not label:
+            label = name
+        tab_elt = TabElement(self, name, label, selected)
+        new_container = container(self.xmlui, tab_elt)
+        return self.xmlui.change_container(new_container)
+
+    def end(self):
+        """ Called when we have finished tabs
+
+        change current container to first container parent
+        """
+        parent_container = self.get_parent_container()
+        self.xmlui.change_container(parent_container)
+
+
+class AdvancedListContainer(Container):
+    """A list which can contain other widgets, headers, etc"""
+
+    type = "advanced_list"
+
+    def __init__(
+        self,
+        xmlui,
+        callback_id=None,
+        name=None,
+        headers=None,
+        items=None,
+        columns=None,
+        selectable="no",
+        auto_index=False,
+        parent=None,
+    ):
+        """Create an advanced list
+
+        @param headers: optional headers information
+        @param callback_id: id of the method to call when selection is done
+        @param items: list of widgets to add (just the first row)
+        @param columns: number of columns in this table, or None to autodetect
+        @param selectable: one of:
+            'no': nothing is done
+            'single': one row can be selected
+        @param auto_index: if True, indexes will be generated by frontends,
+                           starting from 0
+        @return: created element
+        """
+        assert selectable in ("no", "single")
+        if not items and columns is None:
+            raise exceptions.DataError(_("either items or columns need do be filled"))
+        if headers is None:
+            headers = []
+        if items is None:
+            items = []
+        super(AdvancedListContainer, self).__init__(xmlui, parent)
+        if columns is None:
+            columns = len(items[0])
+        self._columns = columns
+        self._item_idx = 0
+        self.current_row = None
+        if headers:
+            if len(headers) != self._columns:
+                raise exceptions.DataError(
+                    _("Headers lenght doesn't correspond to columns")
+                )
+            self.add_headers(headers)
+        if items:
+            self.add_items(items)
+        self.elem.setAttribute("columns", str(self._columns))
+        if callback_id is not None:
+            self.elem.setAttribute("callback", callback_id)
+        self.elem.setAttribute("selectable", selectable)
+        self.auto_index = auto_index
+        if auto_index:
+            self.elem.setAttribute("auto_index", "true")
+        self.next_row_idx = None
+
+    def add_headers(self, headers):
+        for header in headers:
+            self.addHeader(header)
+
+    def addHeader(self, header):
+        pass  # TODO
+
+    def add_items(self, items):
+        for item in items:
+            self.append(item)
+
+    def set_row_index(self, idx):
+        """ Set index for next row
+
+        index are returned when a row is selected, in data's "index" key
+        @param idx: string index to associate to the next row
+        """
+        self.next_row_idx = idx
+
+    def append(self, child):
+        if isinstance(child, RowElement):
+            return super(AdvancedListContainer, self).append(child)
+        if self._item_idx % self._columns == 0:
+            self.current_row = RowElement(self)
+        self.current_row.append(child)
+        self._item_idx += 1
+
+    def end(self):
+        """ Called when we have finished list
+
+        change current container to first container parent
+        """
+        if self._item_idx % self._columns != 0:
+            raise exceptions.DataError(_("Incorrect number of items in list"))
+        parent_container = self.get_parent_container()
+        self.xmlui.change_container(parent_container)
+
+
+## Widgets ##
+
+
+class Widget(Element):
+    type = None
+
+    def __init__(self, xmlui, name=None, parent=None):
+        """Create an element
+
+        @param xmlui: XMLUI instance
+        @param name: name of the element or None
+        @param parent: parent element or None
+        """
+        self.elem = xmlui.doc.createElement("widget")
+        super(Widget, self).__init__(xmlui, parent)
+        if name:
+            self.elem.setAttribute("name", name)
+            if name in xmlui.named_widgets:
+                raise exceptions.ConflictError(
+                    _('A widget with the name "{name}" already exists.').format(
+                        name=name
+                    )
+                )
+            xmlui.named_widgets[name] = self
+        self.elem.setAttribute("type", self.type)
+
+    def set_internal_callback(self, callback, fields, data_elts=None):
+        """Set an internal UI callback when the widget value is changed.
+
+        The internal callbacks are NO callback ids, they are strings from
+        a predefined set of actions that are running in the scope of XMLUI.
+        @param callback (string): a value from:
+            - 'copy': process the widgets given in 'fields' two by two, by
+                copying the values of one widget to the other. Target widgets
+                of type List do not accept the empty value.
+            - 'move': same than copy but moves the values if the source widget
+                is not a List.
+            - 'groups_of_contact': process the widgets two by two, assume A is
+                is a list of JID and B a list of groups, select in B the groups
+                to which the JID selected in A belongs.
+            - more operation to be added when necessary...
+        @param fields (list): a list of widget names (string)
+        @param data_elts (list[Element]): extra data elements
+        """
+        self.elem.setAttribute("internal_callback", callback)
+        if fields:
+            for field in fields:
+                InternalFieldElement(self, field)
+        if data_elts:
+            InternalDataElement(self, data_elts)
+
+
+class EmptyWidget(Widget):
+    """Place holder widget"""
+
+    type = "empty"
+
+
+class TextWidget(Widget):
+    """Used for blob of text"""
+
+    type = "text"
+
+    def __init__(self, xmlui, value, name=None, parent=None):
+        super(TextWidget, self).__init__(xmlui, name, parent)
+        value_elt = self.xmlui.doc.createElement("value")
+        text = self.xmlui.doc.createTextNode(value)
+        value_elt.appendChild(text)
+        self.elem.appendChild(value_elt)
+
+    @property
+    def value(self):
+        return self.elem.firstChild.firstChild.wholeText
+
+
+class LabelWidget(Widget):
+    """One line blob of text
+
+    used most of time to display the desciption or name of the next widget
+    """
+    type = "label"
+
+    def __init__(self, xmlui, label, name=None, parent=None):
+        super(LabelWidget, self).__init__(xmlui, name, parent)
+        self.elem.setAttribute("value", label)
+
+
+class HiddenWidget(Widget):
+    """Not displayed widget, frontends will just copy the value(s)"""
+    type = "hidden"
+
+    def __init__(self, xmlui, value, name, parent=None):
+        super(HiddenWidget, self).__init__(xmlui, name, parent)
+        value_elt = self.xmlui.doc.createElement("value")
+        text = self.xmlui.doc.createTextNode(value)
+        value_elt.appendChild(text)
+        self.elem.appendChild(value_elt)
+
+    @property
+    def value(self):
+        return self.elem.firstChild.firstChild.wholeText
+
+
+class JidWidget(Widget):
+    """Used to display a Jabber ID, some specific methods can be added"""
+
+    type = "jid"
+
+    def __init__(self, xmlui, jid, name=None, parent=None):
+        super(JidWidget, self).__init__(xmlui, name, parent)
+        try:
+            self.elem.setAttribute("value", jid.full())
+        except AttributeError:
+            self.elem.setAttribute("value", str(jid))
+
+    @property
+    def value(self):
+        return self.elem.getAttribute("value")
+
+
+class DividerWidget(Widget):
+    type = "divider"
+
+    def __init__(self, xmlui, style="line", name=None, parent=None):
+        """ Create a divider
+
+        @param xmlui: XMLUI instance
+        @param style: one of:
+            - line: a simple line
+            - dot: a line of dots
+            - dash: a line of dashes
+            - plain: a full thick line
+            - blank: a blank line/space
+        @param name: name of the widget
+        @param parent: parent container
+
+        """
+        super(DividerWidget, self).__init__(xmlui, name, parent)
+        self.elem.setAttribute("style", style)
+
+
+### Inputs ###
+
+
+class InputWidget(Widget):
+    """Widget which can accept user inputs
+
+    used mainly in forms
+    """
+
+    def __init__(self, xmlui, name=None, parent=None, read_only=False):
+        super(InputWidget, self).__init__(xmlui, name, parent)
+        if read_only:
+            self.elem.setAttribute("read_only", "true")
+
+
+class StringWidget(InputWidget):
+    type = "string"
+
+    def __init__(self, xmlui, value=None, name=None, parent=None, read_only=False):
+        super(StringWidget, self).__init__(xmlui, name, parent, read_only=read_only)
+        if value:
+            value_elt = self.xmlui.doc.createElement("value")
+            text = self.xmlui.doc.createTextNode(value)
+            value_elt.appendChild(text)
+            self.elem.appendChild(value_elt)
+
+    @property
+    def value(self):
+        return self.elem.firstChild.firstChild.wholeText
+
+
+class PasswordWidget(StringWidget):
+    type = "password"
+
+
+class TextBoxWidget(StringWidget):
+    type = "textbox"
+
+
+class XHTMLBoxWidget(StringWidget):
+    """Specialized textbox to manipulate XHTML"""
+    type = "xhtmlbox"
+
+    def __init__(self, xmlui, value, name=None, parent=None, read_only=False, clean=True):
+        """
+        @param clean(bool): if True, the XHTML is considered insecure and will be cleaned
+            Only set to False if you are absolutely sure that the XHTML is safe (in other
+            word, set to False only if you made the XHTML yourself)
+        """
+        if clean:
+            if clean_xhtml is None:
+                raise exceptions.NotFound(
+                    "No cleaning method set, can't clean the XHTML")
+            value = clean_xhtml(value)
+
+        super(XHTMLBoxWidget, self).__init__(
+            xmlui, value=value, name=name, parent=parent, read_only=read_only)
+
+
+class JidInputWidget(StringWidget):
+    type = "jid_input"
+
+
+# TODO handle min and max values
+class IntWidget(StringWidget):
+    type = "int"
+
+    def __init__(self, xmlui, value=0, name=None, parent=None, read_only=False):
+        try:
+            int(value)
+        except ValueError:
+            raise exceptions.DataError(_("Value must be an integer"))
+        super(IntWidget, self).__init__(xmlui, value, name, parent, read_only=read_only)
+
+
+class BoolWidget(InputWidget):
+    type = "bool"
+
+    def __init__(self, xmlui, value="false", name=None, parent=None, read_only=False):
+        if isinstance(value, bool):
+            value = "true" if value else "false"
+        elif value == "0":
+            value = "false"
+        elif value == "1":
+            value = "true"
+        if value not in ("true", "false"):
+            raise exceptions.DataError(_("Value must be 0, 1, false or true"))
+        super(BoolWidget, self).__init__(xmlui, name, parent, read_only=read_only)
+        self.elem.setAttribute("value", value)
+
+
+class ButtonWidget(Widget):
+    type = "button"
+
+    def __init__(
+        self, xmlui, callback_id, value=None, fields_back=None, name=None, parent=None
+    ):
+        """Add a button
+
+        @param callback_id: callback which will be called if button is pressed
+        @param value: label of the button
+        @param fields_back: list of names of field to give back when pushing the button
+        @param name: name
+        @param parent: parent container
+        """
+        if fields_back is None:
+            fields_back = []
+        super(ButtonWidget, self).__init__(xmlui, name, parent)
+        self.elem.setAttribute("callback", callback_id)
+        if value:
+            self.elem.setAttribute("value", value)
+        for field in fields_back:
+            FieldBackElement(self, field)
+
+
+class ListWidget(InputWidget):
+    type = "list"
+    STYLES = ("multi", "noselect", "extensible", "reducible", "inline")
+
+    def __init__(
+        self, xmlui, options, selected=None, styles=None, name=None, parent=None
+    ):
+        """
+
+        @param xmlui
+        @param options (list[option]): each option can be given as:
+            - a single string if the label and the value are the same
+            - a tuple with a couple of string (value,label) if the label and the
+              value differ
+        @param selected (list[string]): list of the selected values
+        @param styles (iterable[string]): flags to set the behaviour of the list
+            can be:
+                - multi: multiple selection is allowed
+                - noselect: no selection is allowed
+                    useful when only the list itself is needed
+                - extensible: can be extended by user (i.e. new options can be added)
+                - reducible: can be reduced by user (i.e. options can be removed)
+                - inline: hint that this list should be displayed on a single line
+                          (e.g. list of labels)
+        @param name (string)
+        @param parent
+        """
+        styles = set() if styles is None else set(styles)
+        if styles is None:
+            styles = set()
+        else:
+            styles = set(styles)
+        if "noselect" in styles and ("multi" in styles or selected):
+            raise exceptions.DataError(
+                _(
+                    '"multi" flag and "selected" option are not compatible with '
+                    '"noselect" flag'
+                )
+            )
+        if not options:
+            # we can have no options if we get a submitted data form
+            # but we can't use submitted values directly,
+            # because we would not have the labels
+            log.warning(_('empty "options" list'))
+        super(ListWidget, self).__init__(xmlui, name, parent)
+        self.add_options(options, selected)
+        self.set_styles(styles)
+
+    def add_options(self, options, selected=None):
+        """Add options to a multi-values element (e.g. list) """
+        if selected:
+            if isinstance(selected, str):
+                selected = [selected]
+        else:
+            selected = []
+        for option in options:
+            assert isinstance(option, str) or isinstance(option, tuple)
+            value = option if isinstance(option, str) else option[0]
+            OptionElement(self, option, value in selected)
+
+    def set_styles(self, styles):
+        if not styles.issubset(self.STYLES):
+            raise exceptions.DataError(_("invalid styles"))
+        for style in styles:
+            self.elem.setAttribute(style, "yes")
+        # TODO: check flags incompatibily (noselect and multi) like in __init__
+
+    def setStyle(self, style):
+        self.set_styles([style])
+
+    @property
+    def value(self):
+        """Return the value of first selected option"""
+        for child in self.elem.childNodes:
+            if child.tagName == "option" and child.getAttribute("selected") == "true":
+                return child.getAttribute("value")
+        return ""
+
+
+class JidsListWidget(InputWidget):
+    """A list of text or jids where elements can be added/removed or modified"""
+
+    type = "jids_list"
+
+    def __init__(self, xmlui, jids, styles=None, name=None, parent=None):
+        """
+
+        @param xmlui
+        @param jids (list[jid.JID]): base jids
+        @param styles (iterable[string]): flags to set the behaviour of the list
+        @param name (string)
+        @param parent
+        """
+        super(JidsListWidget, self).__init__(xmlui, name, parent)
+        styles = set() if styles is None else set(styles)
+        if not styles.issubset([]):  # TODO
+            raise exceptions.DataError(_("invalid styles"))
+        for style in styles:
+            self.elem.setAttribute(style, "yes")
+        if not jids:
+            log.debug("empty jids list")
+        else:
+            self.add_jids(jids)
+
+    def add_jids(self, jids):
+        for jid_ in jids:
+            JidElement(self, jid_)
+
+
+## Dialog Elements ##
+
+
+class DialogElement(Element):
+    """Main dialog element """
+
+    type = "dialog"
+
+    def __init__(self, parent, type_, level=None):
+        if not isinstance(parent, TopElement):
+            raise exceptions.DataError(
+                _("DialogElement must be a direct child of TopElement")
+            )
+        super(DialogElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute(C.XMLUI_DATA_TYPE, type_)
+        self.elem.setAttribute(C.XMLUI_DATA_LVL, level or C.XMLUI_DATA_LVL_DEFAULT)
+
+
+class MessageElement(Element):
+    """Element with the instruction message"""
+
+    type = C.XMLUI_DATA_MESS
+
+    def __init__(self, parent, message):
+        if not isinstance(parent, DialogElement):
+            raise exceptions.DataError(
+                _("MessageElement must be a direct child of DialogElement")
+            )
+        super(MessageElement, self).__init__(parent.xmlui, parent)
+        message_txt = self.xmlui.doc.createTextNode(message)
+        self.elem.appendChild(message_txt)
+
+
+class ButtonsElement(Element):
+    """Buttons element which indicate which set to use"""
+
+    type = "buttons"
+
+    def __init__(self, parent, set_):
+        if not isinstance(parent, DialogElement):
+            raise exceptions.DataError(
+                _("ButtonsElement must be a direct child of DialogElement")
+            )
+        super(ButtonsElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute("set", set_)
+
+
+class FileElement(Element):
+    """File element used for FileDialog"""
+
+    type = "file"
+
+    def __init__(self, parent, type_):
+        if not isinstance(parent, DialogElement):
+            raise exceptions.DataError(
+                _("FileElement must be a direct child of DialogElement")
+            )
+        super(FileElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute("type", type_)
+
+
+## XMLUI main class
+
+
+class XMLUI(object):
+    """This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
+
+    def __init__(self, panel_type="window", container="vertical", dialog_opt=None,
+        title=None, submit_id=None, session_id=None):
+        """Init SàT XML Panel
+
+        @param panel_type: one of
+            - C.XMLUI_WINDOW (new window)
+            - C.XMLUI_POPUP
+            - C.XMLUI_FORM (form, depend of the frontend, usually a panel with
+              cancel/submit buttons)
+            - C.XMLUI_PARAM (parameters, presentation depend of the frontend)
+            - C.XMLUI_DIALOG (one common dialog, presentation depend of frontend)
+        @param container: disposition of elements, one of:
+            - vertical: elements are disposed up to bottom
+            - horizontal: elements are disposed left to right
+            - pairs: elements come on two aligned columns
+              (usually one for a label, the next for the element)
+            - label: associations of one LabelWidget or EmptyWidget with an other widget
+                similar to pairs but specialized in LabelWidget,
+                and not necessarily arranged in 2 columns
+            - tabs: elemens are in categories with tabs (notebook)
+        @param dialog_opt: only used if panel_type == C.XMLUI_DIALOG.
+            Dictionnary (string/string) where key can be:
+            - C.XMLUI_DATA_TYPE: type of dialog, value can be:
+                - C.XMLUI_DIALOG_MESSAGE (default): an information/error message.
+                  Action of user is necessary to close the dialog.
+                  Usually the frontend display a classic popup.
+                - C.XMLUI_DIALOG_NOTE: like a C.XMLUI_DIALOG_MESSAGE, but action of user
+                  is not necessary to close, at frontend choice (it can be closed after
+                  a timeout). Usually the frontend display as a timed out notification
+                - C.XMLUI_DIALOG_CONFIRM: dialog with 2 choices (usualy "Ok"/"Cancel").
+                    returned data can contain:
+                        - "answer": "true" if answer is "ok", "yes" or equivalent,
+                                    "false" else
+                - C.XLMUI_DIALOG_FILE: a file selection dialog
+                    returned data can contain:
+                        - "cancelled": "true" if dialog has been cancelled, not present
+                                       or "false" else
+                        - "path": path of the choosed file/dir
+            - C.XMLUI_DATA_MESS: message shown in dialog
+            - C.XMLUI_DATA_LVL: one of:
+                - C.XMLUI_DATA_LVL_INFO (default): normal message
+                - C.XMLUI_DATA_LVL_WARNING: attention of user is important
+                - C.XMLUI_DATA_LVL_ERROR: something went wrong
+            - C.XMLUI_DATA_BTNS_SET: one of:
+                - C.XMLUI_DATA_BTNS_SET_OKCANCEL (default): classical "OK" and "Cancel"
+                  set
+                - C.XMLUI_DATA_BTNS_SET_YESNO: a translated "yes" for OK, and "no" for
+                  Cancel
+            - C.XMLUI_DATA_FILETYPE: only used for file dialogs, one of:
+                - C.XMLUI_DATA_FILETYPE_FILE: a file path is requested
+                - C.XMLUI_DATA_FILETYPE_DIR: a dir path is requested
+                - C.XMLUI_DATA_FILETYPE_DEFAULT: same as C.XMLUI_DATA_FILETYPE_FILE
+
+        @param title: title or default if None
+        @param submit_id: callback id to call for panel_type we can submit (form, param,
+                          dialog)
+        @param session_id: use to keep a session attached to the dialog, must be
+                           returned by frontends
+        @attribute named_widgets(dict): map from name to widget
+        """
+        if panel_type not in [
+            C.XMLUI_WINDOW,
+            C.XMLUI_FORM,
+            C.XMLUI_PARAM,
+            C.XMLUI_POPUP,
+            C.XMLUI_DIALOG,
+        ]:
+            raise exceptions.DataError(_("Unknown panel type [%s]") % panel_type)
+        if panel_type == C.XMLUI_FORM and submit_id is None:
+            raise exceptions.DataError(_("form XMLUI need a submit_id"))
+        if not isinstance(container, str):
+            raise exceptions.DataError(_("container argument must be a string"))
+        if dialog_opt is not None and panel_type != C.XMLUI_DIALOG:
+            raise exceptions.DataError(
+                _("dialog_opt can only be used with dialog panels")
+            )
+        self.type = panel_type
+        impl = minidom.getDOMImplementation()
+
+        self.doc = impl.createDocument(None, "sat_xmlui", None)
+        top_element = self.doc.documentElement
+        top_element.setAttribute("type", panel_type)
+        if title:
+            top_element.setAttribute("title", title)
+        self.submit_id = submit_id
+        self.session_id = session_id
+        if panel_type == C.XMLUI_DIALOG:
+            if dialog_opt is None:
+                dialog_opt = {}
+            self._create_dialog(dialog_opt)
+            return
+        self.main_container = self._create_container(container, TopElement(self))
+        self.current_container = self.main_container
+        self.named_widgets = {}
+
+    @staticmethod
+    def creator_wrapper(widget_cls, is_input):
+        # TODO: once moved to Python 3, use functools.partialmethod and
+        #       remove the creator_wrapper
+        def create_widget(self, *args, **kwargs):
+            if self.type == C.XMLUI_DIALOG:
+                raise exceptions.InternalError(_(
+                    "create_widget can't be used with dialogs"))
+            if "parent" not in kwargs:
+                kwargs["parent"] = self.current_container
+            if "name" not in kwargs and is_input:
+                # name can be given as first argument or in keyword
+                # arguments for InputWidgets
+                args = list(args)
+                kwargs["name"] = args.pop(0)
+            return widget_cls(self, *args, **kwargs)
+        return create_widget
+
+    @classmethod
+    def _introspect(cls):
+        """ Introspect module to find Widgets and Containers, and create addXXX methods"""
+        # FIXME: we can't log anything because this file is used
+        #        in bin/sat script then evaluated
+        #        bin/sat should be refactored
+        # log.debug(u'introspecting XMLUI widgets and containers')
+        cls._containers = {}
+        cls._widgets = {}
+        for obj in list(globals().values()):
+            try:
+                if issubclass(obj, Widget):
+                    if obj.__name__ == "Widget":
+                        continue
+                    cls._widgets[obj.type] = obj
+                    creator_name = "add" + obj.__name__
+                    if creator_name.endswith('Widget'):
+                        creator_name = creator_name[:-6]
+                    is_input = issubclass(obj, InputWidget)
+                    # FIXME: cf. above comment
+                    # log.debug(u"Adding {creator_name} creator (is_input={is_input}))"
+                    #     .format(creator_name=creator_name, is_input=is_input))
+
+                    assert not hasattr(cls, creator_name)
+                    # XXX: we need to use creator_wrapper because we are in a loop
+                    #      and Python 2 doesn't support default values in kwargs
+                    #      when using *args, **kwargs
+                    setattr(cls, creator_name, cls.creator_wrapper(obj, is_input))
+
+                elif issubclass(obj, Container):
+                    if obj.__name__ == "Container":
+                        continue
+                    cls._containers[obj.type] = obj
+            except TypeError:
+                pass
+
+    def __del__(self):
+        self.doc.unlink()
+
+    @property
+    def submit_id(self):
+        top_element = self.doc.documentElement
+        if not top_element.hasAttribute("submit"):
+            # getAttribute never return None (it return empty string it attribute doesn't exists)
+            # so we have to manage None here
+            return None
+        value = top_element.getAttribute("submit")
+        return value
+
+    @submit_id.setter
+    def submit_id(self, value):
+        top_element = self.doc.documentElement
+        if value is None:
+            try:
+                top_element.removeAttribute("submit")
+            except NotFoundErr:
+                pass
+        else:  # submit_id can be the empty string to bypass form restriction
+            top_element.setAttribute("submit", value)
+
+    @property
+    def session_id(self):
+        top_element = self.doc.documentElement
+        value = top_element.getAttribute("session_id")
+        return value or None
+
+    @session_id.setter
+    def session_id(self, value):
+        top_element = self.doc.documentElement
+        if value is None:
+            try:
+                top_element.removeAttribute("session_id")
+            except NotFoundErr:
+                pass
+        elif value:
+            top_element.setAttribute("session_id", value)
+        else:
+            raise exceptions.DataError("session_id can't be empty")
+
+    def _create_dialog(self, dialog_opt):
+        dialog_type = dialog_opt.setdefault(C.XMLUI_DATA_TYPE, C.XMLUI_DIALOG_MESSAGE)
+        if (
+            dialog_type in [C.XMLUI_DIALOG_CONFIRM, C.XMLUI_DIALOG_FILE]
+            and self.submit_id is None
+        ):
+            raise exceptions.InternalError(
+                _("Submit ID must be filled for this kind of dialog")
+            )
+        top_element = TopElement(self)
+        level = dialog_opt.get(C.XMLUI_DATA_LVL)
+        dialog_elt = DialogElement(top_element, dialog_type, level)
+
+        try:
+            MessageElement(dialog_elt, dialog_opt[C.XMLUI_DATA_MESS])
+        except KeyError:
+            pass
+
+        try:
+            ButtonsElement(dialog_elt, dialog_opt[C.XMLUI_DATA_BTNS_SET])
+        except KeyError:
+            pass
+
+        try:
+            FileElement(dialog_elt, dialog_opt[C.XMLUI_DATA_FILETYPE])
+        except KeyError:
+            pass
+
+    def _create_container(self, container, parent=None, **kwargs):
+        """Create a container element
+
+        @param type: container type (cf init doc)
+        @parent: parent element or None
+        """
+        if container not in self._containers:
+            raise exceptions.DataError(_("Unknown container type [%s]") % container)
+        cls = self._containers[container]
+        new_container = cls(self, parent=parent, **kwargs)
+        return new_container
+
+    def change_container(self, container, **kwargs):
+        """Change the current container
+
+        @param container: either container type (container it then created),
+                          or an Container instance"""
+        if isinstance(container, str):
+            self.current_container = self._create_container(
+                container,
+                self.current_container.get_parent_container() or self.main_container,
+                **kwargs
+            )
+        else:
+            self.current_container = (
+                self.main_container if container is None else container
+            )
+        assert isinstance(self.current_container, Container)
+        return self.current_container
+
+    def add_widget(self, type_, *args, **kwargs):
+        """Convenience method to add an element"""
+        if "parent" not in kwargs:
+            kwargs["parent"] = self.current_container
+        try:
+            cls = self._widgets[type_]
+        except KeyError:
+            raise exceptions.DataError(_("Invalid type [{type_}]").format(type_=type_))
+        return cls(self, *args, **kwargs)
+
+    def toXml(self):
+        """return the XML representation of the panel"""
+        return self.doc.toxml()
+
+
+# we call this to have automatic discovery of containers and widgets
+XMLUI._introspect()
+
+
+# Some sugar for XMLUI dialogs
+
+
+def note(message, title="", level=C.XMLUI_DATA_LVL_INFO):
+    """sugar to easily create a Note Dialog
+
+    @param message(unicode): body of the note
+    @param title(unicode): title of the note
+    @param level(unicode): one of C.XMLUI_DATA_LVL_*
+    @return(XMLUI): instance of XMLUI
+    """
+    note_xmlui = XMLUI(
+        C.XMLUI_DIALOG,
+        dialog_opt={
+            C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
+            C.XMLUI_DATA_MESS: message,
+            C.XMLUI_DATA_LVL: level,
+        },
+        title=title,
+    )
+    return note_xmlui
+
+
+def quick_note(host, client, message, title="", level=C.XMLUI_DATA_LVL_INFO):
+    """more sugar to do the whole note process"""
+    note_ui = note(message, title, level)
+    host.action_new({"xmlui": note_ui.toXml()}, profile=client.profile)
+
+
+def deferred_ui(host, xmlui, chained=False):
+    """create a deferred linked to XMLUI
+
+    @param xmlui(XMLUI): instance of the XMLUI
+        Must be an XMLUI that you can submit, with submit_id set to ''
+    @param chained(bool): True if the Deferred result must be returned to the frontend
+        useful when backend is in a series of dialogs with an ui
+    @return (D(data)): a deferred which fire the data
+    """
+    assert xmlui.submit_id == ""
+    xmlui_d = defer.Deferred()
+
+    def on_submit(data, profile):
+        xmlui_d.callback(data)
+        return xmlui_d if chained else {}
+
+    xmlui.submit_id = host.register_callback(on_submit, with_data=True, one_shot=True)
+    return xmlui_d
+
+
+def defer_xmlui(host, xmlui, action_extra=None, security_limit=C.NO_SECURITY_LIMIT,
+    chained=False, profile=C.PROF_KEY_NONE):
+    """Create a deferred linked to XMLUI
+
+    @param xmlui(XMLUI): instance of the XMLUI
+        Must be an XMLUI that you can submit, with submit_id set to ''
+    @param profile: %(doc_profile)s
+    @param action_extra(None, dict): extra action to merge with xmlui
+        mainly used to add meta informations (see action_new doc)
+    @param security_limit: %(doc_security_limit)s
+    @param chained(bool): True if the Deferred result must be returned to the frontend
+        useful when backend is in a series of dialogs with an ui
+    @return (data): a deferred which fire the data
+    """
+    xmlui_d = deferred_ui(host, xmlui, chained)
+    action_data = {"xmlui": xmlui.toXml()}
+    if action_extra is not None:
+        action_data.update(action_extra)
+    host.action_new(
+        action_data,
+        security_limit=security_limit,
+        keep_id=xmlui.submit_id,
+        profile=profile,
+    )
+    return xmlui_d
+
+
+def defer_dialog(
+    host,
+    message: str,
+    title: str = "Please confirm",
+    type_: str = C.XMLUI_DIALOG_CONFIRM,
+    options: Optional[dict] = None,
+    action_extra: Optional[dict] = None,
+    security_limit: int = C.NO_SECURITY_LIMIT,
+    chained: bool = False,
+    profile: str = C.PROF_KEY_NONE
+) -> defer.Deferred:
+    """Create a submitable dialog and manage it with a deferred
+
+    @param message: message to display
+    @param title: title of the dialog
+    @param type: dialog type (C.XMLUI_DIALOG_* or plugin specific string)
+    @param options: if not None, will be used to update (extend) dialog_opt arguments of
+        XMLUI
+    @param action_extra: extra action to merge with xmlui
+        mainly used to add meta informations (see action_new doc)
+    @param security_limit: %(doc_security_limit)s
+    @param chained: True if the Deferred result must be returned to the frontend
+        useful when backend is in a series of dialogs with an ui
+    @param profile: %(doc_profile)s
+    @return: answer dict
+    """
+    assert profile is not None
+    dialog_opt = {"type": type_, "message": message}
+    if options is not None:
+        dialog_opt.update(options)
+    dialog = XMLUI(C.XMLUI_DIALOG, title=title, dialog_opt=dialog_opt, submit_id="")
+    return defer_xmlui(host, dialog, action_extra, security_limit, chained, profile)
+
+
+def defer_confirm(*args, **kwargs):
+    """call defer_dialog and return a boolean instead of the whole data dict"""
+    d = defer_dialog(*args, **kwargs)
+    d.addCallback(lambda data: C.bool(data["answer"]))
+    return d
+
+
+# Misc other funtions
+
+def element_copy(
+    element: domish.Element,
+    with_parent: bool = True,
+    with_children: bool = True
+) -> domish.Element:
+    """Make a copy of a domish.Element
+
+    The copy will have its own children list, so other elements
+    can be added as direct children without modifying orignal one.
+    Children are not deeply copied, so if an element is added to a child or grandchild,
+    it will also affect original element.
+    @param element: Element to clone
+    """
+    new_elt = domish.Element(
+        (element.uri, element.name),
+        defaultUri = element.defaultUri,
+        attribs = element.attributes,
+        localPrefixes = element.localPrefixes)
+    if with_parent:
+        new_elt.parent = element.parent
+    if with_children:
+        new_elt.children = element.children[:]
+    return new_elt
+
+
+def is_xhtml_field(field):
+    """Check if a data_form.Field is an XHTML one"""
+    return (field.fieldType is None and field.ext_type == "xml" and
+            field.value.uri == C.NS_XHTML)
+
+
+class ElementParser:
+    """Callable class to parse XML string into Element"""
+
+    # XXX: Found at http://stackoverflow.com/questions/2093400/how-to-create-twisted-words-xish-domish-element-entirely-from-raw-xml/2095942#2095942
+
+    def _escape_html(self, matchobj):
+        entity = matchobj.group(1)
+        if entity in XML_ENTITIES:
+            # we don't escape XML entities
+            return matchobj.group(0)
+        else:
+            try:
+                return chr(html.entities.name2codepoint[entity])
+            except KeyError:
+                log.warning("removing unknown entity {}".format(entity))
+                return ""
+
+    def __call__(self, raw_xml, force_spaces=False, namespace=None):
+        """
+        @param raw_xml(unicode): the raw XML
+        @param force_spaces (bool): if True, replace occurrences of '\n' and '\t'
+                                    with ' '.
+        @param namespace(unicode, None): if set, use this namespace for the wrapping
+                                         element
+        """
+        # we need to wrap element in case
+        # there is not a unique one on the top
+        if namespace is not None:
+            raw_xml = "<div xmlns='{}'>{}</div>".format(namespace, raw_xml)
+        else:
+            raw_xml = "<div>{}</div>".format(raw_xml)
+
+        # avoid ParserError on HTML escaped chars
+        raw_xml = html_entity_re.sub(self._escape_html, raw_xml)
+
+        self.result = None
+
+        def on_start(elem):
+            self.result = elem
+
+        def on_end():
+            pass
+
+        def onElement(elem):
+            self.result.addChild(elem)
+
+        parser = domish.elementStream()
+        parser.DocumentStartEvent = on_start
+        parser.ElementEvent = onElement
+        parser.DocumentEndEvent = on_end
+        tmp = domish.Element((None, "s"))
+        if force_spaces:
+            raw_xml = raw_xml.replace("\n", " ").replace("\t", " ")
+        tmp.addRawXml(raw_xml)
+        parser.parse(tmp.toXml().encode("utf-8"))
+        top_elt = self.result.firstChildElement()
+        # we now can check if there was a unique element on the top
+        # and remove our wrapping <div/> is this is the case
+        top_elt_children = list(top_elt.elements())
+        if len(top_elt_children) == 1:
+            top_elt = top_elt_children[0]
+        return top_elt
+
+
+parse = ElementParser()
+
+
+# FIXME: this method is duplicated from frontends.tools.xmlui.get_text
+def get_text(node):
+    """Get child text nodes of a domish.Element.
+
+    @param node (domish.Element)
+    @return: joined unicode text of all nodes
+    """
+    data = []
+    for child in node.childNodes:
+        if child.nodeType == child.TEXT_NODE:
+            data.append(child.wholeText)
+    return "".join(data)
+
+
+def find_all(elt, namespaces=None, names=None):
+    """Find child element at any depth matching criteria
+
+    @param elt(domish.Element): top parent of the elements to find
+    @param names(iterable[unicode], basestring, None): names to match
+        None to accept every names
+    @param namespace(iterable[unicode], basestring, None): URIs to match
+        None to accept every namespaces
+    @return ((G)domish.Element): found elements
+    """
+    if isinstance(namespaces, str):
+        namespaces = tuple((namespaces,))
+    if isinstance(names, str):
+        names = tuple((names,))
+
+    for child in elt.elements():
+        if (
+            domish.IElement.providedBy(child)
+            and (not names or child.name in names)
+            and (not namespaces or child.uri in namespaces)
+        ):
+            yield child
+        for found in find_all(child, namespaces, names):
+            yield found
+
+
+def find_ancestor(
+    elt,
+    name: str,
+    namespace: Optional[Union[str, Iterable[str]]] = None
+    ) -> domish.Element:
+    """Retrieve ancestor of an element
+
+    @param elt: starting element, its parent will be checked recursively until the
+        required one if found
+    @param name: name of the element to find
+    @param namespace: namespace of the element to find
+        - None to find any element with that name
+        - a simple string to find the namespace
+        - several namespaces can be specified in an iterable, if an element with any of
+          this namespace and given name is found, it will match
+
+    """
+    if isinstance(namespace, str):
+        namespace = [namespace]
+    current = elt.parent
+    while True:
+        if current is None:
+            raise exceptions.NotFound(
+                f"Can't find any ancestor {name!r} (xmlns: {namespace!r})"
+            )
+        if current.name == name and (namespace is None or current.uri in namespace):
+            return current
+        current = current.parent
+
+
+def p_fmt_elt(elt, indent=0, defaultUri=""):
+    """Pretty format a domish.Element"""
+    strings = []
+    for child in elt.children:
+        if domish.IElement.providedBy(child):
+            strings.append(p_fmt_elt(child, indent+2, defaultUri=elt.defaultUri))
+        else:
+            strings.append(f"{(indent+2)*' '}{child!s}")
+    if elt.children:
+        nochild_elt = domish.Element(
+            (elt.uri, elt.name), elt.defaultUri, elt.attributes, elt.localPrefixes
+        )
+        strings.insert(0, f"{indent*' '}{nochild_elt.toXml(defaultUri=defaultUri)[:-2]}>")
+        strings.append(f"{indent*' '}</{nochild_elt.name}>")
+    else:
+        strings.append(f"{indent*' '}{elt.toXml(defaultUri=defaultUri)}")
+    return '\n'.join(strings)
+
+
+def pp_elt(elt):
+    """Pretty print a domish.Element"""
+    print(p_fmt_elt(elt))
+
+
+# ElementTree
+
+def et_get_namespace_and_name(et_elt: ET.Element) -> Tuple[Optional[str], str]:
+    """Retrieve element namespace and name from ElementTree element
+
+    @param et_elt: ElementTree element
+    @return: namespace and name of the element
+        if not namespace if specified, None is returned
+    """
+    name = et_elt.tag
+    if not name:
+        raise ValueError("no name set in ET element")
+    elif name[0] != "{":
+        return None, name
+    end_idx = name.find("}")
+    if end_idx == -1:
+        raise ValueError("Invalid ET name")
+    return name[1:end_idx], name[end_idx+1:]
+
+
+def et_elt_2_domish_elt(et_elt: Union[ET.Element, etree.Element]) -> domish.Element:
+    """Convert ElementTree element to Twisted's domish.Element
+
+    Note: this is a naive implementation, adapted to XMPP, and some content are ignored
+        (attributes namespaces, tail)
+    """
+    namespace, name = et_get_namespace_and_name(et_elt)
+    elt = domish.Element((namespace, name), attribs=et_elt.attrib)
+    if et_elt.text:
+        elt.addContent(et_elt.text)
+    for child in et_elt:
+        elt.addChild(et_elt_2_domish_elt(child))
+    return elt
+
+
+@overload
+def domish_elt_2_et_elt(elt: domish.Element, lxml: Literal[False]) -> ET.Element:
+    ...
+
+@overload
+def domish_elt_2_et_elt(elt: domish.Element, lxml: Literal[True]) -> etree.Element:
+    ...
+
+@overload
+def domish_elt_2_et_elt(
+    elt: domish.Element, lxml: bool
+) -> Union[ET.Element, etree.Element]:
+    ...
+
+def domish_elt_2_et_elt(elt, lxml = False):
+    """Convert Twisted's domish.Element to ElementTree equivalent
+
+    Note: this is a naive implementation, adapted to XMPP, and some text content may be
+        missing (content put after a tag, i.e. what would go to the "tail" attribute of ET
+        Element)
+    """
+    tag = f"{{{elt.uri}}}{elt.name}" if elt.uri else elt.name
+    if lxml:
+        et_elt = etree.Element(tag, attr=elt.attributes)
+    else:
+        et_elt = ET.Element(tag, attrib=elt.attributes)
+    content = str(elt)
+    if content:
+        et_elt.text = str(elt)
+    for child in elt.elements():
+        et_elt.append(domish_elt_2_et_elt(child, lxml=lxml))
+    return et_elt
+
+
+def domish_elt_2_et_elt2(element: domish.Element) -> ET.Element:
+    """
+    WIP, originally from the OMEMO plugin
+    """
+
+    element_name = element.name
+    if element.uri is not None:
+        element_name = "{" + element.uri + "}" + element_name
+
+    attrib: Dict[str, str] = {}
+    for qname, value in element.attributes.items():
+        attribute_name = qname[1] if isinstance(qname, tuple) else qname
+        attribute_namespace = qname[0] if isinstance(qname, tuple) else None
+        if attribute_namespace is not None:
+            attribute_name = "{" + attribute_namespace + "}" + attribute_name
+
+        attrib[attribute_name] = value
+
+    result = ET.Element(element_name, attrib)
+
+    last_child: Optional[ET.Element] = None
+    for child in element.children:
+        if isinstance(child, str):
+            if last_child is None:
+                result.text = child
+            else:
+                last_child.tail = child
+        else:
+            last_child = domish_elt_2_et_elt2(child)
+            result.append(last_child)
+
+    return result