# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from markupsafe import Markup from odoo.addons.mail.tests import common from odoo.exceptions import AccessError from odoo.tests import tagged, users @tagged('mail_render') class TestMailRender(common.MailCommon): @classmethod def setUpClass(cls): super(TestMailRender, cls).setUpClass() # activate multi language support cls.env['res.lang']._activate_lang('fr_FR') cls.user_admin.write({'lang': 'en_US'}) # test records cls.render_object = cls.env['res.partner'].create({ 'name': 'TestRecord', 'lang': 'en_US', }) cls.render_object_fr = cls.env['res.partner'].create({ 'name': 'Element de Test', 'lang': 'fr_FR', }) # some jinja templates cls.base_inline_template_bits = [ '

Hello

', '

Hello {{ object.name }}

', """

{{ 'English Speaker' if object.lang == 'en_US' else 'Other Speaker' }}

""", """

{{ 13 + 13 }}

This is a test

""", """ Test {{ '' if True else 'Code not executed' }} """, ] cls.base_inline_template_bits_fr = [ '

Bonjour

', '

Bonjour {{ object.name }}

', """

{{ 'Narrateur Anglais' if object.lang == 'en_US' else 'Autre Narrateur' }}

""" ] # some qweb templates, their views and their xml ids cls.base_qweb_bits = [ '

Hello

', '

Hello

', """

English Speaker Other Speaker

""" ] cls.base_qweb_bits_fr = [ '

Bonjour

', '

Bonjour

', """

Narrateur Anglais Autre Narrateur

""" ] cls.base_qweb_templates = cls.env['ir.ui.view'].create([ {'name': 'TestRender%d' % index, 'type': 'qweb', 'arch': qweb_content, } for index, qweb_content in enumerate(cls.base_qweb_bits) ]) cls.base_qweb_templates_data = cls.env['ir.model.data'].create([ {'name': template.name, 'module': 'mail', 'model': template._name, 'res_id': template.id, } for template in cls.base_qweb_templates ]) cls.base_qweb_templates_xmlids = [ model_data.complete_name for model_data in cls.base_qweb_templates_data ] # render result cls.base_rendered = [ '

Hello

', '

Hello %s

' % cls.render_object.name, """

English Speaker

""" ] cls.base_rendered_fr = [ '

Bonjour

', '

Bonjour %s

' % cls.render_object_fr.name, """

Autre Narrateur

""" ] # link to mail template cls.test_template = cls.env['mail.template'].create({ 'name': 'Test Template', 'subject': cls.base_inline_template_bits[0], 'body_html': cls.base_qweb_bits[1], 'model_id': cls.env['ir.model']._get('res.partner').id, 'lang': '{{ object.lang }}' }) # some translations cls.env['ir.translation'].create({ 'type': 'model', 'name': 'mail.template,subject', 'lang': 'fr_FR', 'res_id': cls.test_template.id, 'src': cls.test_template.subject, 'value': cls.base_qweb_bits_fr[0], }) cls.env['ir.translation'].create({ 'type': 'model', 'name': 'mail.template,body_html', 'lang': 'fr_FR', 'res_id': cls.test_template.id, 'src': cls.test_template.body_html, 'value': cls.base_qweb_bits_fr[1], }) cls.env['ir.model.data'].create({ 'name': 'test_template_xmlid', 'module': 'mail', 'model': cls.test_template._name, 'res_id': cls.test_template.id, }) # Enable group-based template management cls.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', True) # User without the group "mail.group_mail_template_editor" cls.user_rendering_restricted = common.mail_new_test_user( cls.env, login='user_rendering_restricted', groups='base.group_user', company_id=cls.company_admin.id, name='Code Template Restricted User', notification_type='inbox', signature='--\nErnest' ) cls.user_rendering_restricted.groups_id -= cls.env.ref('mail.group_mail_template_editor') @users('employee') def test_evaluation_context(self): """ Test evaluation context and various ways of tweaking it. """ partner = self.env['res.partner'].browse(self.render_object.ids) MailRenderMixin = self.env['mail.render.mixin'] custom_ctx = {'custom_ctx': 'Custom Context Value'} add_context = { 'custom_value': 'Custom Render Value' } srces = [ 'I am {{ user.name }}', 'Datetime is {{ format_datetime(datetime.datetime(2021, 6, 1), dt_format="MM - d - YYY") }}', 'Context {{ ctx.get("custom_ctx") }}, value {{ custom_value }}', ] results = [ 'I am %s' % self.env.user.name, 'Datetime is 06 - 1 - 2021', 'Context Custom Context Value, value Custom Render Value' ] for src, expected in zip(srces, results): for engine in ['inline_template']: result = MailRenderMixin.with_context(**custom_ctx)._render_template( src, partner._name, partner.ids, engine=engine, add_context=add_context )[partner.id] self.assertEqual(expected, result) @users('employee') def test_render_field(self): template = self.env['mail.template'].browse(self.test_template.ids) partner = self.env['res.partner'].browse(self.render_object.ids) for fname, expected in zip(['subject', 'body_html'], self.base_rendered): rendered = template._render_field( fname, partner.ids, compute_lang=True )[partner.id] self.assertEqual(rendered, expected) partner = self.env['res.partner'].browse(self.render_object_fr.ids) for fname, expected in zip(['subject', 'body_html'], self.base_rendered_fr): rendered = template._render_field( fname, partner.ids, compute_lang=True )[partner.id] self.assertEqual(rendered, expected) @users('employee') def test_render_template_inline_template(self): partner = self.env['res.partner'].browse(self.render_object.ids) for source, expected in zip(self.base_inline_template_bits, self.base_rendered): rendered = self.env['mail.render.mixin']._render_template( source, partner._name, partner.ids, engine='inline_template', )[partner.id] self.assertEqual(rendered, expected) @users('employee') def test_render_template_local_links(self): local_links_template_bits = [ '
', '
', '
', ] base_url = self.env['mail.render.mixin'].get_base_url() rendered_local_links = [ '
' % base_url, '
' % base_url, '
' % base_url ] for source, expected in zip(local_links_template_bits, rendered_local_links): rendered = self.env['mail.render.mixin']._replace_local_links(source) self.assertEqual(rendered, expected) @users('employee') def test_render_template_qweb(self): partner = self.env['res.partner'].browse(self.render_object.ids) for source, expected in zip(self.base_qweb_bits, self.base_rendered): rendered = self.env['mail.render.mixin']._render_template( source, partner._name, partner.ids, engine='qweb', )[partner.id] self.assertEqual(rendered, expected) @users('employee') def test_render_template_qweb_view(self): partner = self.env['res.partner'].browse(self.render_object.ids) for source, expected in zip(self.base_qweb_templates_xmlids, self.base_rendered): rendered = self.env['mail.render.mixin']._render_template( source, partner._name, partner.ids, engine='qweb_view', )[partner.id] self.assertEqual(rendered, expected) @users('employee') def test_template_rendering_impersonate(self): """ Test that the use of SUDO do not change the current user. """ partner = self.env['res.partner'].browse(self.render_object.ids) src = '{{ user.name }} - {{ object.name }}' expected = '%s - %s' % (self.env.user.name, partner.name) result = self.env['mail.render.mixin'].sudo()._render_template_inline_template( src, partner._name, partner.ids )[partner.id] self.assertIn(expected, result) @users('user_rendering_restricted') def test_template_rendering_function_call(self): """Test the case when the template call a custom function. This function should not be called when the template is not rendered. """ model = 'res.partner' res_ids = self.env[model].search([], limit=1).ids partner = self.env[model].browse(res_ids) MailRenderMixin = self.env['mail.render.mixin'] def cust_function(): # Can not use "MagicMock" in a Jinja sand-boxed environment # so create our own function cust_function.call = True return 'return value' cust_function.call = False src = """

This is a test

{{ cust_function() }}

""" expected = """

This is a test

return value

""" context = {'cust_function': cust_function} result = self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_inline_template( src, partner._name, partner.ids, add_context=context )[partner.id] self.assertEqual(expected, result) self.assertTrue(cust_function.call) with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'): MailRenderMixin._render_template_inline_template(src, model, res_ids, add_context=context) @users('user_rendering_restricted') def test_template_render_static(self): """Test that we render correctly static templates (without placeholders).""" model = 'res.partner' res_ids = self.env[model].search([], limit=1).ids MailRenderMixin = self.env['mail.render.mixin'] result = MailRenderMixin._render_template_inline_template(self.base_inline_template_bits[0], model, res_ids)[res_ids[0]] self.assertEqual(result, self.base_inline_template_bits[0]) @users('user_rendering_restricted') def test_template_rendering_restricted(self): """Test if we correctly detect static template.""" res_ids = self.env['res.partner'].search([], limit=1).ids with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'): self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[3], 'res.partner', res_ids) @users('employee') def test_template_rendering_unrestricted(self): """Test if we correctly detect static template.""" res_ids = self.env['res.partner'].search([], limit=1).ids result = self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[3], 'res.partner', res_ids)[res_ids[0]] self.assertIn('26', result, 'Template Editor should be able to render inline_template code') @users('employee') def test_template_rendering_various(self): """ Test static rendering """ partner = self.env['res.partner'].browse(self.render_object.ids) MailRenderMixin = self.env['mail.render.mixin'] # static string src = 'This is a string' expected = 'This is a string' for engine in ['inline_template']: result = MailRenderMixin._render_template( src, partner._name, partner.ids, engine=engine, )[partner.id] self.assertEqual(expected, result) # code string src = 'This is a string with a number {{ 13+13 }}' expected = 'This is a string with a number 26' for engine in ['inline_template']: result = MailRenderMixin._render_template( src, partner._name, partner.ids, engine=engine, )[partner.id] self.assertEqual(expected, result) # block string src = "This is a string with a block {{ 'hidden' if False else 'displayed' }}" expected = 'This is a string with a block displayed' for engine in ['inline_template']: result = MailRenderMixin._render_template( src, partner._name, partner.ids, engine=engine, )[partner.id] self.assertEqual(expected, result) # static xml src = '

This is a string

' expected = '

This is a string

' for engine in ['inline_template', 'qweb']: result = MailRenderMixin._render_template( src, partner._name, partner.ids, engine=engine, )[partner.id] self.assertEqual(expected, result) # tde: checkme # code xml srces = [ '

This is a string with a number {{ 13+13 }}

', '

This is a string with a number

', ] expected = '

This is a string with a number 26

' for engine, src in zip(['inline_template', 'qweb'], srces): result = MailRenderMixin._render_template( src, partner._name, partner.ids, engine=engine, )[partner.id] self.assertEqual(expected, str(result)) src = """

We have cookies in stock We have cookies in stock

""" expected = """

We have 3 cookies in stock We have 4 cookies in stock

""" for engine in ['qweb']: result = MailRenderMixin._render_template( src, partner._name, partner.ids, engine=engine, )[partner.id] self.assertEqual(result, expected) @users('user_rendering_restricted') def test_security_inline_template_restricted(self): """Test if we correctly detect condition block (which might contains code).""" res_ids = self.env['res.partner'].search([], limit=1).ids with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'): self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids) @users('employee') def test_is_inline_template_condition_block_unrestricted(self): """Test if we correctly detect condition block (which might contains code).""" res_ids = self.env['res.partner'].search([], limit=1).ids result = self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids)[res_ids[0]] self.assertNotIn('Code not executed', result, 'The condition block did not work') @users('user_rendering_restricted') def test_security_qweb_template_restricted(self): """Test if we correctly detect condition block (which might contains code).""" res_ids = self.env['res.partner'].search([], limit=1).ids with self.assertRaises(AccessError, msg='Simple user should not be able to render qweb code'): self.env['mail.render.mixin']._render_template_qweb(self.base_qweb_bits[1], 'res.partner', res_ids) @users('user_rendering_restricted') def test_security_qweb_template_restricted_cached(self): """Test if we correctly detect condition block (which might contains code).""" res_ids = self.env['res.partner'].search([], limit=1).ids # Render with the admin first to fill the cache self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_qweb( self.base_qweb_bits[1], 'res.partner', res_ids) # Check that it raise even when rendered previously by an admin with self.assertRaises(AccessError, msg='Simple user should not be able to render qweb code'): self.env['mail.render.mixin']._render_template_qweb( self.base_qweb_bits[1], 'res.partner', res_ids) @users('employee') def test_security_qweb_template_unrestricted(self): """Test if we correctly detect condition block (which might contains code).""" res_ids = self.env['res.partner'].search([], limit=1).ids result = self.env['mail.render.mixin']._render_template_qweb(self.base_qweb_bits[1], 'res.partner', res_ids)[res_ids[0]] self.assertNotIn('Code not executed', result, 'The condition block did not work') @users('user_rendering_restricted') def test_template_rendering_static_inline_template(self): model = 'res.partner' res_ids = self.env[model].search([], limit=1).ids partner = self.env[model].browse(res_ids) src = """

This is a static template

""" result = self.env['mail.render.mixin']._render_template_inline_template( src, model, res_ids)[partner.id] self.assertEqual(src, str(result)) @users('user_rendering_restricted') def test_template_rendering_static_qweb(self): model = 'res.partner' res_ids = self.env[model].search([], limit=1).ids partner = self.env[model].browse(res_ids) src = """

This is a static template

""" result = self.env['mail.render.mixin']._render_template_qweb(src, model, res_ids)[ partner.id] self.assertEqual(src, str(result)) @users('employee') def test_prepend_preview_inline_template_to_qweb(self): body = 'body' preview = 'foo{{"false" if 1 > 2 else "true"}}bar' result = self.env['mail.render.mixin']._prepend_preview(Markup(body), preview) self.assertEqual(result, '''
foobar
body''') def test_mail_template_neutralize(self): """ ensure mail templates can be neutralized """ fake_mail_server = self.env['ir.mail_server'].create({ 'name': "fake test email server", 'smtp_host': "mail.example.com", 'smtp_port': 15626, }) self.test_template.mail_server_id = fake_mail_server self.env['mail.template']._neutralize() self.assertFalse(self.test_template.mail_server_id) # bonus test mail server neutralize too self.assertTrue(fake_mail_server.active) self.env['ir.mail_server']._neutralize() self.assertFalse(fake_mail_server.active)