diff --git a/odoo/addons/test_testing_utilities/tests/__init__.py b/odoo/addons/test_testing_utilities/tests/__init__.py index 7be7831430492ebb80643d6db2a2262c5189c27a..7193c4e5f94f3d7bb931dce6730d01d2d66d4307 100644 --- a/odoo/addons/test_testing_utilities/tests/__init__.py +++ b/odoo/addons/test_testing_utilities/tests/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import test_methods +from . import test_lxml from . import test_form_impl diff --git a/odoo/addons/test_testing_utilities/tests/test_lxml.py b/odoo/addons/test_testing_utilities/tests/test_lxml.py new file mode 100644 index 0000000000000000000000000000000000000000..fac6013bd675f9c5884e1d4ffc8296862e573f55 --- /dev/null +++ b/odoo/addons/test_testing_utilities/tests/test_lxml.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import common +from odoo.tools.xml_utils import _check_with_xsd + +import base64 +from lxml.etree import XMLSchemaError + +class TestLXML(common.TransactionCase): + def test_lxml_import_from_filestore(self): + resolver_schema_int = b""" + <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:etype="http://codespeak.net/lxml/test/external"> + <xsd:import namespace="http://codespeak.net/lxml/test/external" schemaLocation="imported_schema.xsd"/> + <xsd:element name="a" type="etype:AType"/> + </xsd:schema> + """ + + incomplete_schema_int = b""" + <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:etype="http://codespeak.net/lxml/test/external"> + <xsd:import namespace="http://codespeak.net/lxml/test/external" schemaLocation="non_existing_schema.xsd"/> + <xsd:element name="a" type="etype:AType"/> + </xsd:schema> + """ + + imported_schema = b""" + <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://codespeak.net/lxml/test/external"> + <xsd:complexType name="AType"> + <xsd:sequence><xsd:element name="b" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/></xsd:sequence> + </xsd:complexType> + </xsd:schema> + """ + + self.env['ir.attachment'].create([{ + 'datas': base64.b64encode(resolver_schema_int), + 'name': 'resolver_schema_int.xsd' + }, { + 'datas': base64.b64encode(incomplete_schema_int), + 'name': 'incomplete_schema_int.xsd' + }, { + 'datas': base64.b64encode(imported_schema), + 'name': 'imported_schema.xsd' + }]) + + _check_with_xsd("<a><b></b></a>", 'resolver_schema_int.xsd', self.env) + + with self.assertRaises(XMLSchemaError): + _check_with_xsd("<a><b></b></a>", 'incomplete_schema_int.xsd', self.env) + + with self.assertRaises(FileNotFoundError): + _check_with_xsd("<a><b></b></a>", 'non_existing_schema.xsd', self.env) diff --git a/odoo/tools/xml_utils.py b/odoo/tools/xml_utils.py index eb5b8b203bff0020bce3cf4350f04a16290779d8..513aa20766352a6879f27a04deced200c9673538 100644 --- a/odoo/tools/xml_utils.py +++ b/odoo/tools/xml_utils.py @@ -1,22 +1,56 @@ # -*- coding: utf-8 -*- +import base64 +from io import BytesIO from lxml import etree -from odoo.tools.misc import file_open + from odoo.exceptions import UserError + +class odoo_resolver(etree.Resolver): + """Odoo specific file resolver that can be added to the XML Parser. + + It will search filenames in the ir.attachments + """ + + def __init__(self, env): + super().__init__() + self.env = env + + def resolve(self, url, id, context): + """Search url in ``ir.attachment`` and return the resolved content.""" + attachment = self.env['ir.attachment'].search([('name', '=', url)]) + if attachment: + return self.resolve_string(base64.b64decode(attachment.datas), context) + + def check_with_xsd(tree_or_str, stream): raise UserError("Method 'check_with_xsd' deprecated ") +def _check_with_xsd(tree_or_str, stream, env=None): + """Check an XML against an XSD schema. -def _check_with_xsd(tree_or_str, stream): + This will raise a UserError if the XML file is not valid according to the + XSD file. + :param tree_or_str (etree, str): representation of the tree to be checked + :param stream (io.IOBase, str): the byte stream used to build the XSD schema. + If env is given, it can also be the name of an attachment in the filestore + :param env (odoo.api.Environment): If it is given, it enables resolving the + imports of the schema in the filestore with ir.attachments. + """ if not isinstance(tree_or_str, etree._Element): tree_or_str = etree.fromstring(tree_or_str) - xml_schema_doc = etree.parse(stream) - xsd_schema = etree.XMLSchema(xml_schema_doc) + parser = etree.XMLParser() + if env: + parser.resolvers.add(odoo_resolver(env)) + if isinstance(stream, str) and stream.endswith('.xsd'): + attachment = env['ir.attachment'].search([('name', '=', stream)]) + if not attachment: + raise FileNotFoundError() + stream = BytesIO(base64.b64decode(attachment.datas)) + xsd_schema = etree.XMLSchema(etree.parse(stream, parser=parser)) try: xsd_schema.assertValid(tree_or_str) except etree.DocumentInvalid as xml_errors: - #import UserError only here to avoid circular import statements with tools.func being imported in exceptions.py - from odoo.exceptions import UserError raise UserError('\n'.join(str(e) for e in xml_errors.error_log))