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