diff sat/tools/xml_tools.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/tools/xml_tools.py@65695b9343d3
children 56f94936df1e
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/tools/xml_tools.py	Mon Apr 02 19:44:50 2018 +0200
@@ -0,0 +1,1521 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# SAT: a jabber client
+# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.i18n import _
+from sat.core.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
+from copy import deepcopy
+import htmlentitydefs
+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')
+
+# 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 == '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, 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)
+        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 = form_xml.elements('jabber:x:data', 'reported').next()
+    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(u"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):
+    """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]
+    @return: XMLUI instance
+    """
+    form = deepcopy(result_form)
+    for name, field in form.fields.iteritems():
+        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=True, 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, 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
+    """
+    # 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 <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 = [elem.getAttribute("value") for elem in elems]
+    selected = [elem.getAttribute("value") for elem in elems if elem.getAttribute("selected") == 'true']
+    return (options, selected)
+
+def _paramsGetListJids(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.setSelected(selected)
+
+    def setSelected(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, basestring):
+            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_, basestring):
+            value = unicode(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 getParentContainer(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):
+    type = "pairs"
+
+
+class LabelContainer(Container):
+    type = "label"
+
+
+class TabsContainer(Container):
+    type = "tabs"
+
+    def addTab(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.changeContainer(new_container)
+
+    def end(self):
+        """ Called when we have finished tabs
+
+        change current container to first container parent
+        """
+        parent_container = self.getParentContainer()
+        self.xmlui.changeContainer(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.addHeaders(headers)
+        if items:
+            self.addItems(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 addHeaders(self, headers):
+        for header in headers:
+            self.addHeader(header)
+
+    def addHeader(self, header):
+        pass  # TODO
+
+    def addItems(self, items):
+        for item in items:
+            self.append(item)
+
+    def setRowIndex(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.getParentContainer()
+        self.xmlui.changeContainer(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(_(u'A widget with the name "{name}" already exists.').format(name=name))
+            xmlui.named_widgets[name] = self
+        self.elem.setAttribute('type', self.type)
+
+    def setInternalCallback(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 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', unicode(jid))
+
+
+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 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 = (u'multi', u'noselect', u'extensible', u'reducible', u'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 u'noselect' in styles and (u'multi' in styles or selected):
+                raise exceptions.DataError(_(u'"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.addOptions(options, selected)
+        self.setStyles(styles)
+
+    def addOptions(self, options, selected=None):
+        """Add options to a multi-values element (e.g. list) """
+        if selected:
+            if isinstance(selected, basestring):
+                selected = [selected]
+        else:
+            selected = []
+        for option in options:
+            assert isinstance(option, basestring) or isinstance(option, tuple)
+            value = option if isinstance(option, basestring) else option[0]
+            OptionElement(self, option, value in selected)
+
+    def setStyles(self, styles):
+        if not styles.issubset(self.STYLES):
+            raise exceptions.DataError(_(u"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.setStyles([style])
+
+    @property
+    def value(self):
+        """Return the value of first selected option"""
+        for child in self.elem.childNodes:
+            if child.tagName == u'option' and child.getAttribute(u'selected') == u'true':
+                return child.getAttribute(u'value')
+        return u''
+
+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.addJids(jids)
+
+    def addJids(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
+        """
+        self._introspect() # FIXME: why doing that on each XMLUI ? should be done once
+        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, basestring):
+            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._createDialog(dialog_opt)
+            return
+        self.main_container = self._createContainer(container, TopElement(self))
+        self.current_container = self.main_container
+        self.named_widgets = {}
+
+    def _introspect(self):
+        """ Introspect module to find Widgets and Containers """
+        self._containers = {}
+        self._widgets = {}
+        for obj in globals().values():
+            try:
+                if issubclass(obj, Widget):
+                    if obj.__name__ == 'Widget':
+                        continue
+                    self._widgets[obj.type] = obj
+                elif issubclass(obj, Container):
+                    if obj.__name__ == 'Container':
+                        continue
+                    self._containers[obj.type] = obj
+            except TypeError:
+                pass
+
+    def __del__(self):
+        self.doc.unlink()
+
+    def __getattr__(self, name):
+        if name.startswith("add") and name not in ('addWidget',):  # addWidgetName(...) create an instance of WidgetName
+            if self.type == C.XMLUI_DIALOG:
+                raise exceptions.InternalError(_("addXXX can't be used with dialogs"))
+            class_name = name[3:] + "Widget"
+            if class_name in globals():
+                cls = globals()[class_name]
+                if issubclass(cls, Widget):
+                    def createWidget(*args, **kwargs):
+                        if "parent" not in kwargs:
+                            kwargs["parent"] = self.current_container
+                        if "name" not in kwargs and issubclass(cls, InputWidget):  # name can be given as first argument or in keyword arguments for InputWidgets
+                            args = list(args)
+                            kwargs["name"] = args.pop(0)
+                        return cls(self, *args, **kwargs)
+                    return createWidget
+        return object.__getattribute__(self, name)
+
+    @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 _createDialog(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 _createContainer(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 changeContainer(self, container, **kwargs):
+        """Change the current container
+
+        @param container: either container type (container it then created),
+                          or an Container instance"""
+        if isinstance(container, basestring):
+            self.current_container = self._createContainer(container, self.current_container.getParentContainer() 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 addWidget(self, type_, *args, **kwargs):
+        """Convenience method to add an element"""
+        if type_ not in self._widgets:
+            raise exceptions.DataError(_("Invalid type [%s]") % type_)
+        if "parent" not in kwargs:
+            kwargs["parent"] = self.current_container
+        cls = self._widgets[type_]
+        return cls(self, *args, **kwargs)
+
+    def toXml(self):
+        """return the XML representation of the panel"""
+        return self.doc.toxml()
+
+
+# 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 quickNote(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.actionNew({'xmlui': note_ui.toXml()}, profile=client.profile)
+
+
+def deferredUI(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 onSubmit(data, profile):
+        xmlui_d.callback(data)
+        return xmlui_d if chained else {}
+
+    xmlui.submit_id = host.registerCallback(onSubmit, with_data=True, one_shot=True)
+    return xmlui_d
+
+def deferXMLUI(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 actionNew 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 = deferredUI(host, xmlui, chained)
+    action_data = {'xmlui': xmlui.toXml()}
+    if action_extra is not None:
+        action_data.update(action_extra)
+    host.actionNew(action_data, security_limit=security_limit, keep_id=xmlui.submit_id, profile=profile)
+    return xmlui_d
+
+def deferDialog(host, message, title=u'Please confirm', type_=C.XMLUI_DIALOG_CONFIRM, options=None,
+        action_extra=None, security_limit=C.NO_SECURITY_LIMIT, chained=False, profile=C.PROF_KEY_NONE):
+    """Create a submitable dialog and manage it with a deferred
+
+    @param message(unicode): message to display
+    @param title(unicode): title of the dialog
+    @param type(unicode): dialog type (C.XMLUI_DIALOG_*)
+    @param options(None, dict): if not None, will be used to update (extend) dialog_opt arguments of XMLUI
+    @param action_extra(None, dict): extra action to merge with xmlui
+        mainly used to add meta informations (see actionNew 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
+    @param profile: %(doc_profile)s
+    @return (dict): Deferred 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 deferXMLUI(host, dialog, action_extra, security_limit, chained, profile)
+
+def deferConfirm(*args, **kwargs):
+    """call deferDialog and return a boolean instead of the whole data dict"""
+    d = deferDialog(*args, **kwargs)
+    d.addCallback(lambda data: C.bool(data['answer']))
+    return d
+
+# Misc other funtions
+
+class ElementParser(object):
+    """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 _escapeHTML(self, matchobj):
+        entity = matchobj.group(1)
+        if entity in XML_ENTITIES:
+            # we don't escape XML entities
+            return matchobj.group(0)
+        else:
+            try:
+                return unichr(htmlentitydefs.name2codepoint[entity])
+            except KeyError:
+                log.warning(u"removing unknown entity {}".format(entity))
+                return u''
+
+    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 = u"<div xmlns='{}'>{}</div>".format(namespace, raw_xml)
+        else:
+            raw_xml = u"<div>{}</div>".format(raw_xml)
+
+        # avoid ParserError on HTML escaped chars
+        raw_xml = html_entity_re.sub(self._escapeHTML, raw_xml)
+
+        self.result = None
+
+        def onStart(elem):
+            self.result = elem
+
+        def onEnd():
+            pass
+
+        def onElement(elem):
+            self.result.addChild(elem)
+
+        parser = domish.elementStream()
+        parser.DocumentStartEvent = onStart
+        parser.ElementEvent = onElement
+        parser.DocumentEndEvent = onEnd
+        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 was the case
+        if len(top_elt.children) == 1 and domish.IElement.providedBy(top_elt.children[0]):
+            top_elt = top_elt.firstChildElement()
+        return top_elt
+
+
+# FIXME: this method is duplicated from frontends.tools.xmlui.getText
+def getText(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 u"".join(data)
+
+def findAll(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, basestring):
+        namespaces=tuple((namespaces,))
+    if isinstance(names, basestring):
+        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 findAll(child, namespaces, names):
+            yield found