1# -*- coding: utf-8 -*-
2"""Utilities for generating, parsing and checking XML/XSD files on top of the lxml.etree module."""
3
4import base64
5from io import BytesIO
6from lxml import etree
7
8from odoo.exceptions import UserError
9
10
11class odoo_resolver(etree.Resolver):
12    """Odoo specific file resolver that can be added to the XML Parser.
13
14    It will search filenames in the ir.attachments
15    """
16
17    def __init__(self, env):
18        super().__init__()
19        self.env = env
20
21    def resolve(self, url, id, context):
22        """Search url in ``ir.attachment`` and return the resolved content."""
23        attachment = self.env['ir.attachment'].search([('name', '=', url)])
24        if attachment:
25            return self.resolve_string(base64.b64decode(attachment.datas), context)
26
27
28def _check_with_xsd(tree_or_str, stream, env=None):
29    """Check an XML against an XSD schema.
30
31    This will raise a UserError if the XML file is not valid according to the
32    XSD file.
33    :param tree_or_str (etree, str): representation of the tree to be checked
34    :param stream (io.IOBase, str): the byte stream used to build the XSD schema.
35        If env is given, it can also be the name of an attachment in the filestore
36    :param env (odoo.api.Environment): If it is given, it enables resolving the
37        imports of the schema in the filestore with ir.attachments.
38    """
39    if not isinstance(tree_or_str, etree._Element):
40        tree_or_str = etree.fromstring(tree_or_str)
41    parser = etree.XMLParser()
42    if env:
43        parser.resolvers.add(odoo_resolver(env))
44        if isinstance(stream, str) and stream.endswith('.xsd'):
45            attachment = env['ir.attachment'].search([('name', '=', stream)])
46            if not attachment:
47                raise FileNotFoundError()
48            stream = BytesIO(base64.b64decode(attachment.datas))
49    xsd_schema = etree.XMLSchema(etree.parse(stream, parser=parser))
50    try:
51        xsd_schema.assertValid(tree_or_str)
52    except etree.DocumentInvalid as xml_errors:
53        raise UserError('\n'.join(str(e) for e in xml_errors.error_log))
54
55
56def create_xml_node_chain(first_parent_node, nodes_list, last_node_value=None):
57    """Generate a hierarchical chain of nodes.
58
59    Each new node being the child of the previous one based on the tags contained
60    in `nodes_list`, under the given node `first_parent_node`.
61    :param first_parent_node (etree._Element): parent of the created tree/chain
62    :param nodes_list (iterable<str>): tag names to be created
63    :param last_node_value (str): if specified, set the last node's text to this value
64    :returns (list<etree._Element>): the list of created nodes
65    """
66    res = []
67    current_node = first_parent_node
68    for tag in nodes_list:
69        current_node = etree.SubElement(current_node, tag)
70        res.append(current_node)
71
72    if last_node_value is not None:
73        current_node.text = last_node_value
74    return res
75
76
77def create_xml_node(parent_node, node_name, node_value=None):
78    """Create a new node.
79
80    :param parent_node (etree._Element): parent of the created node
81    :param node_name (str): name of the created node
82    :param node_value (str): value of the created node (optional)
83    :returns (etree._Element):
84    """
85    return create_xml_node_chain(parent_node, [node_name], node_value)[0]
86