Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package trytond for openSUSE:Factory checked in at 2026-08-14 22:09:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/trytond (Old) and /work/SRC/openSUSE:Factory/.trytond.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "trytond" Fri Aug 14 22:09:25 2026 rev:119 rq:1371190 version:7.0.55 Changes: -------- --- /work/SRC/openSUSE:Factory/trytond/trytond.changes 2026-06-12 19:28:57.937808912 +0200 +++ /work/SRC/openSUSE:Factory/.trytond.new.1258/trytond.changes 2026-08-14 22:09:36.704695754 +0200 @@ -1,0 +2,5 @@ +Fri Aug 14 09:23:34 UTC 2026 - Axel Braun <[email protected]> + +- Version 7.0.55 - Bugfix Release + +------------------------------------------------------------------- Old: ---- trytond-7.0.51.tar.gz New: ---- trytond-7.0.55.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ trytond.spec ++++++ --- /var/tmp/diff_new_pack.VBsB9k/_old 2026-08-14 22:09:37.601728653 +0200 +++ /var/tmp/diff_new_pack.VBsB9k/_new 2026-08-14 22:09:37.603728726 +0200 @@ -30,7 +30,7 @@ ##%%endif Name: trytond -Version: %{majorver}.51 +Version: %{majorver}.55 Release: 0 Summary: An Enterprise Resource Planning (ERP) system License: GPL-3.0-or-later ++++++ trytond-7.0.51.tar.gz -> trytond-7.0.55.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/CHANGELOG new/trytond-7.0.55/CHANGELOG --- old/trytond-7.0.51/CHANGELOG 2026-06-02 18:46:47.000000000 +0200 +++ new/trytond-7.0.55/CHANGELOG 2026-08-04 12:05:58.000000000 +0200 @@ -1,4 +1,26 @@ +Version 7.0.55 - 2026-08-04 +--------------------------- +* Bug fixes (see mercurial logs for details) +* Restrict protocols allowed for weasyprint to http(s) (issue14947) + +Version 7.0.54 - 2026-07-15 +--------------------------- +* Bug fixes (see mercurial logs for details) + + +Version 7.0.53 - 2026-07-01 +--------------------------- +* Bug fixes (see mercurial logs for details) +* Enforce access right on the email template record (issue14907) +* Add ModelAccessProxy (issue14907) +* Restrict Genshi evaluation (issue14869, issue5160) + +Version 7.0.52 - 2026-06-18 +--------------------------- +* Bug fixes (see mercurial logs for details) + + Version 7.0.51 - 2026-06-02 --------------------------- * Bug fixes (see mercurial logs for details) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/PKG-INFO new/trytond-7.0.55/PKG-INFO --- old/trytond-7.0.51/PKG-INFO 2026-06-02 18:46:51.011286300 +0200 +++ new/trytond-7.0.55/PKG-INFO 2026-08-04 12:06:02.040366600 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: trytond -Version: 7.0.51 +Version: 7.0.55 Summary: Tryton server Home-page: http://www.tryton.org/ Download-URL: http://downloads.tryton.org/7.0/ @@ -77,7 +77,7 @@ Provides-Extra: html2text Requires-Dist: html2text; extra == "html2text" Provides-Extra: weasyprint -Requires-Dist: weasyprint; extra == "weasyprint" +Requires-Dist: weasyprint>=68.0; extra == "weasyprint" Provides-Extra: coroutine Requires-Dist: gevent>=1.1; extra == "coroutine" Provides-Extra: image diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/doc/ref/models.rst new/trytond-7.0.55/doc/ref/models.rst --- old/trytond-7.0.51/doc/ref/models.rst 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/doc/ref/models.rst 2026-07-08 08:36:10.000000000 +0200 @@ -775,6 +775,15 @@ the string +ModelAccessProxy +================ + +.. class:: ModelAccessProxy(record[, context]) + + A class proxying instance of :class:`ModelStorage` with check access using + ``context``. + + convert_from ------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/setup.py new/trytond-7.0.55/setup.py --- old/trytond-7.0.51/setup.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/setup.py 2026-08-04 10:39:24.000000000 +0200 @@ -154,7 +154,7 @@ 'BCrypt': ['passlib[bcrypt]'], 'Argon2': ['passlib[argon2]'], 'html2text': ['html2text'], - 'weasyprint': ['weasyprint'], + 'weasyprint': ['weasyprint >= 68.0'], 'coroutine': ['gevent>=1.1'], 'image': ['pillow'], 'barcode': ['python-barcode[images]'], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/tox.ini new/trytond-7.0.55/tox.ini --- old/trytond-7.0.51/tox.ini 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/tox.ini 2026-07-30 08:12:46.000000000 +0200 @@ -16,7 +16,7 @@ deps = coverage unittest-xml-reporting - postgresql: psycopg2 >= 2.7.0 + postgresql: psycopg2-binary >= 2.7.0 passenv = * setenv = sqlite: TRYTOND_DATABASE_URI={env:SQLITE_URI:sqlite://} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/__init__.py new/trytond-7.0.55/trytond/__init__.py --- old/trytond-7.0.51/trytond/__init__.py 2026-05-20 23:22:06.000000000 +0200 +++ new/trytond-7.0.55/trytond/__init__.py 2026-07-15 18:24:00.000000000 +0200 @@ -10,7 +10,9 @@ import __main__ from lxml import etree, objectify -__version__ = "7.0.51" +from ._safe_genshi import genshi_patch + +__version__ = "7.0.55" if not os.environ.get('TRYTOND_APPNAME'): os.environ['TRYTOND_APPNAME'] = os.path.basename( @@ -31,6 +33,8 @@ etree.set_default_parser(etree.XMLParser(resolve_entities=False)) objectify.set_default_parser(objectify.makeparser(resolve_entities=False)) +genshi_patch() + class _RequestPatchFinder: def find_spec(self, fullname, path, target=None): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/_safe_genshi.py new/trytond-7.0.55/trytond/_safe_genshi.py --- old/trytond-7.0.51/trytond/_safe_genshi.py 1970-01-01 01:00:00.000000000 +0100 +++ new/trytond-7.0.55/trytond/_safe_genshi.py 2026-07-14 19:21:23.000000000 +0200 @@ -0,0 +1,108 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +from genshi.template.astutil import ASTTransformer +from genshi.template.eval import ( + BUILTINS, Code, ExpressionASTTransformer, TemplateASTTransformer) + + +class _SafeASTTransformer(ASTTransformer): + + def visit_Attribute(self, node): + if (node.attr.startswith('_') + and node.attr not in { + '__class__', '__name__', '__url__', '__href__'}): + raise ValueError(f"invalid attribute {node.attr!r}") + return super().visit_Attribute(node) + + def visit_Import(self, node): + raise ValueError("invalid import") + + def visit_ImportFrom(self, node): + raise ValueError("invalid import from") + + def visit_Name(self, node): + if (node.id.startswith('_') + and not node.id.startswith('__relatorio_') + and node.id not in {'_'}): + raise ValueError(f"invalid name {node.id!r}") + return super().visit_Name(node) + + +class SafeExpressionASTTransformer( + _SafeASTTransformer, ExpressionASTTransformer): + pass + + +class SafeTemplateASTTransformer(_SafeASTTransformer, TemplateASTTransformer): + pass + + +ALLOWED_BUILTINS = { + 'False', + 'True', + 'None', + 'abs', + 'all', + 'any', + 'ascii', + 'bin', + 'bool', + 'bytearray', + 'bytes', + 'chr', + 'complex', + 'dict', + 'dir', + 'divmod', + 'enumerate', + 'filter', + 'float', + 'format', + 'frozenset', + 'hasattr', + 'hash', + 'hex', + 'int', + 'iter', + 'len', + 'list', + 'map', + 'max', + 'min', + 'next', + 'oct', + 'ord', + 'pow', + 'range', + 'repr', + 'reversed', + 'round', + 'set', + 'slice', + 'sorted', + 'str', + 'sum', + 'tuple', + 'zip', + } + + +def genshi_patch(): + + code__init__ = Code.__init__ + + def patched_code__init__( + self, source, filename=None, lineno=-1, lookup='strict', + xform=None): + if self.mode == 'eval': + xform = SafeExpressionASTTransformer + else: + xform = SafeTemplateASTTransformer + code__init__( + self, source, filename=filename, lineno=lineno, lookup=lookup, + xform=xform) + Code.__init__ = patched_code__init__ + + for name in BUILTINS.keys() - ALLOWED_BUILTINS: + BUILTINS.pop(name) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/action.py new/trytond-7.0.55/trytond/ir/action.py --- old/trytond-7.0.51/trytond/ir/action.py 2025-12-14 17:59:46.000000000 +0100 +++ new/trytond-7.0.55/trytond/ir/action.py 2026-07-30 23:56:51.000000000 +0200 @@ -484,7 +484,7 @@ 'get_report_content', setter='set_report_content') report_content_name = fields.Function(fields.Char('Content Name'), 'on_change_with_report_content_name') - report_content_html = fields.Function(fields.Binary( + report_content_html = fields.Function(fields.Text( "Content HTML", states={ 'invisible': ~Eval('template_extension').in_( @@ -646,11 +646,18 @@ @classmethod def get_report_content_html(cls, reports, name): - return cls.get_report_content(reports, name[:-5]) + contents = cls.get_report_content(reports, name[:-5]) + for report_id, content in contents.items(): + if isinstance(content, bytes): + try: + contents[report_id] = content.decode('utf-8') + except UnicodeDecodeError: + pass + return contents @classmethod def set_report_content_html(cls, reports, name, value): - if value is not None: + if isinstance(value, str): value = value.encode('utf-8') cls.set_report_content(reports, name[:-5], value) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/email_.py new/trytond-7.0.55/trytond/ir/email_.py --- old/trytond-7.0.51/trytond/ir/email_.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/trytond/ir/email_.py 2026-07-08 08:36:10.000000000 +0200 @@ -19,7 +19,8 @@ from trytond.config import config from trytond.i18n import gettext -from trytond.model import EvalEnvironment, ModelSQL, ModelView, fields +from trytond.model import ( + EvalEnvironment, ModelAccessProxy, ModelSQL, ModelView, fields) from trytond.model.exceptions import AccessError, ValidationError from trytond.pool import Pool from trytond.pyson import Bool, Eval, PYSONDecoder @@ -418,7 +419,7 @@ def get(self, record): pool = Pool() Model = pool.get(self.model.model) - record = Model(int(record)) + record = ModelAccessProxy(Model(int(record))) values = {} for attr, key in [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/lang.py new/trytond-7.0.55/trytond/ir/lang.py --- old/trytond-7.0.51/trytond/ir/lang.py 2026-04-14 18:58:51.000000000 +0200 +++ new/trytond-7.0.55/trytond/ir/lang.py 2026-06-16 15:37:57.000000000 +0200 @@ -4,6 +4,7 @@ import datetime from ast import literal_eval from decimal import Decimal +from functools import partial from itertools import takewhile from locale import CHAR_MAX @@ -506,6 +507,7 @@ Formats val according to the currency settings in lang. """ # Code from currency in locale.py + conv = partial(getattr, self) # check for illegal values if digits is None: @@ -522,10 +524,8 @@ if symbol: smb = currency.symbol - precedes = (val < 0 and self.n_cs_precedes - or self.p_cs_precedes) - separated = (val < 0 and self.n_sep_by_space - or self.p_sep_by_space) + precedes = conv(val < 0 and 'n_cs_precedes' or 'p_cs_precedes') + separated = conv(val < 0 and 'n_sep_by_space' or 'p_sep_by_space') if not smb and hasattr(currency, 'code'): smb = currency.code separated = True @@ -535,8 +535,8 @@ else: s = s + (separated and ' ' or '') + smb - sign_pos = val < 0 and self.n_sign_posn or self.p_sign_posn - sign = val < 0 and self.negative_sign or self.positive_sign + sign_pos = conv(val < 0 and 'n_sign_posn' or 'p_sign_posn') + sign = conv(val < 0 and 'negative_sign' or 'positive_sign') if sign_pos == 0: s = '(' + s + ')' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/model.py new/trytond-7.0.55/trytond/ir/model.py --- old/trytond-7.0.51/trytond/ir/model.py 2026-02-27 18:14:46.000000000 +0100 +++ new/trytond-7.0.55/trytond/ir/model.py 2026-07-08 08:36:10.000000000 +0200 @@ -530,13 +530,12 @@ @classmethod def get_access(cls, models): 'Return access for models' - # root user above constraint - if Transaction().user == 0: - return defaultdict(lambda: defaultdict(lambda: True)) - pool = Pool() Model = pool.get('ir.model') - User = pool.get('res.user') + try: + User = pool.get('res.user') + except KeyError: + return defaultdict(lambda: defaultdict(lambda: True)) cursor = Transaction().connection.cursor() model_access = cls.__table__() ir_model = Model.__table__() @@ -627,9 +626,8 @@ Model = pool.get(model_name) assert mode in ['read', 'write', 'create', 'delete'], \ 'Invalid access mode for security' - transaction = Transaction() - if (transaction.user == 0 - or (raise_exception and not transaction.check_access)): + + if not Transaction().check_access: return True access = cls.get_access([model_name])[model_name][mode] @@ -749,15 +747,14 @@ @classmethod def get_access(cls, models): 'Return fields access for models' - # root user above constraint - if Transaction().user == 0: - return defaultdict(lambda: defaultdict( - lambda: defaultdict(lambda: True))) - pool = Pool() Model = pool.get('ir.model') ModelField = pool.get('ir.model.field') - User = pool.get('res.user') + try: + User = pool.get('res.user') + except KeyError: + return defaultdict(lambda: defaultdict( + lambda: defaultdict(lambda: True))) field_access = cls.__table__() ir_model = Model.__table__() model_field = ModelField.__table__() @@ -807,8 +804,7 @@ return accesses @classmethod - def check(cls, model_name, fields, mode='read', raise_exception=True, - access=False): + def check(cls, model_name, fields, mode='read', raise_exception=True): ''' Check access for fields on model_name. ''' @@ -816,17 +812,12 @@ Model = pool.get(model_name) assert mode in ('read', 'write', 'create', 'delete'), \ 'Invalid access mode' - transaction = Transaction() - if (transaction.user == 0 - or (raise_exception and not transaction.check_access)): - if access: - return dict((x, True) for x in fields) + + if not Transaction().check_access: return True accesses = dict((f, a[mode]) for f, a in cls.get_access([model_name])[model_name].items()) - if access: - return accesses for field in fields: if not accesses.get(field, True): if raise_exception: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/resource.py new/trytond-7.0.55/trytond/ir/resource.py --- old/trytond-7.0.51/trytond/ir/resource.py 2026-05-31 17:47:57.000000000 +0200 +++ new/trytond-7.0.55/trytond/ir/resource.py 2026-07-08 08:36:10.000000000 +0200 @@ -69,13 +69,14 @@ @classmethod def search( cls, domain, offset=0, limit=None, order=None, count=False, - query=False): + **kwargs): transaction = Transaction() enforce_access = ( - not query and transaction.user and transaction.check_access) + not kwargs.get('query', False) + and transaction.user and transaction.check_access) result = super().search( domain, offset=offset, limit=limit, order=order, - count=False if enforce_access else count, query=query) + count=False if enforce_access else count, **kwargs) if not enforce_access: return result @@ -110,7 +111,7 @@ loop += 1 result = super().search( domain, offset=offset + loop * limit, limit=limit, order=order, - count=False, query=False) + count=False) if not result: break diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/ir/rule.py new/trytond-7.0.55/trytond/ir/rule.py --- old/trytond-7.0.51/trytond/ir/rule.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/trytond/ir/rule.py 2026-07-08 08:36:10.000000000 +0200 @@ -7,10 +7,11 @@ from trytond.cache import Cache from trytond.i18n import gettext from trytond.model import Check, Index, ModelSQL, ModelView, fields -from trytond.model.exceptions import ValidationError +from trytond.model.exceptions import AccessError, ValidationError from trytond.pool import Pool from trytond.pyson import PYSONDecoder -from trytond.transaction import Transaction, inactive_records +from trytond.transaction import ( + Transaction, inactive_records, without_check_access) class DomainError(ValidationError): @@ -205,9 +206,6 @@ model_names = list(model_names) cursor = transaction.connection.cursor() - # root user above constraint - if transaction.user == 0: - return {}, {} cursor.execute(*rule_table.join(rule_group, condition=rule_group.id == rule_table.rule_group ).join(model, @@ -236,8 +234,8 @@ clause = defaultdict(lambda: ['OR']) clause_global = defaultdict(lambda: ['OR']) - # Use root user without context to prevent recursion - with transaction.set_user(0), transaction.set_context(user=0): + # Without check access to prevent recursion + with without_check_access(): rules = cls.browse(ids) for rule in rules: decoder = PYSONDecoder( @@ -265,9 +263,8 @@ @classmethod def domain_get(cls, model_name, mode='read'): pool = Pool() - transaction = Transaction() - # root user above constraint - if transaction.user == 0 or not transaction.check_access: + + if not Transaction().check_access: return [] assert mode in cls.modes @@ -312,6 +309,47 @@ return Model.search(domain, order=[], query=True) @classmethod + def check(cls, model_name, ids, mode='read'): + pool = Pool() + Model = pool.get(model_name) + transaction = Transaction() + + def test_domain(ids, domain): + # Use root to prevent infinite recursion + with transaction.set_user(0, set_context=True), \ + inactive_records(), \ + without_check_access(): + records = Model.search([ + ('id', 'in', ids), + domain, + ], order=[]) + return list(set(ids).difference(map(int, records))) + + domain = cls.domain_get(model_name, mode=mode) + if not domain: + return + forbidden = test_domain(ids, domain) + if forbidden: + ids = ', '.join(map(str, forbidden[:5])) + if len(forbidden) > 5: + ids += '...' + rules = [] + clause, clause_global = cls.get(model_name, mode=mode) + if clause: + dom = list(clause.values()) + dom.insert(0, 'OR') + if test_domain(forbidden, dom): + rules.extend(clause.keys()) + for rule, dom in clause_global.items(): + if test_domain(forbidden, dom): + rules.append(rule) + raise AccessError(gettext( + f'ir.msg_{mode}_rule_error', + ids=ids, + rules='\n'.join(r.name for r in rules), + **Model.__names__())) + + @classmethod def delete(cls, rules): super(Rule, cls).delete(rules) # Restart the cache on the domain_get method of ir.rule diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/model/__init__.py new/trytond-7.0.55/trytond/model/__init__.py --- old/trytond-7.0.51/trytond/model/__init__.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/trytond/model/__init__.py 2026-07-08 08:36:10.000000000 +0200 @@ -9,7 +9,7 @@ from .model import Model from .modelsingleton import ModelSingleton from .modelsql import Check, Exclude, Index, ModelSQL, Unique, convert_from -from .modelstorage import EvalEnvironment, ModelStorage +from .modelstorage import EvalEnvironment, ModelAccessProxy, ModelStorage from .modelview import ModelView from .multivalue import MultiValueMixin, ValueMixin from .order import sequence_ordered, sort @@ -23,4 +23,4 @@ 'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod', 'MultiValueMixin', 'ValueMixin', 'SymbolMixin', 'DigitsMixin', 'EvalEnvironment', 'sequence_ordered', 'sort', 'DeactivableMixin', 'tree', - 'sum_tree', 'avatar_mixin'] + 'sum_tree', 'avatar_mixin', 'ModelAccessProxy'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/model/fields/many2one.py new/trytond-7.0.55/trytond/model/fields/many2one.py --- old/trytond-7.0.51/trytond/model/fields/many2one.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/trytond/model/fields/many2one.py 2026-07-08 08:36:10.000000000 +0200 @@ -250,7 +250,10 @@ target_id, = query.columns if isinstance(target_id, As): target_id = target_id.expression - query.where &= target_id == column + if query.where is None: + query.where = target_id == column + else: + query.where &= target_id == column expression = column.in_(query) if operator.startswith('not'): return ~expression diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/model/modelstorage.py new/trytond-7.0.55/trytond/model/modelstorage.py --- old/trytond-7.0.51/trytond/model/modelstorage.py 2026-02-27 21:27:54.000000000 +0100 +++ new/trytond-7.0.55/trytond/model/modelstorage.py 2026-07-08 08:36:10.000000000 +0200 @@ -28,7 +28,8 @@ from trytond.tools.domain_inversion import domain_inversion, eval_domain from trytond.tools.domain_inversion import parse as domain_parse from trytond.transaction import ( - Transaction, inactive_records, record_cache_size, without_check_access) + Transaction, check_access, inactive_records, record_cache_size, + without_check_access) from . import fields from .descriptors import dualmethod @@ -1705,11 +1706,11 @@ if load_eager or multiple_getter: FieldAccess = Pool().get('ir.model.field.access') - fread_accesses = {} - fread_accesses.update(FieldAccess.check(self.__name__, - list(self._fields.keys()), 'read', access=True)) - to_remove = set(x for x, y in fread_accesses.items() - if not y and x != name) + fields_access = FieldAccess.get_access( + [self.__name__])[self.__name__] + to_remove = { + f for f, a in fields_access.items() if not a['read']} + to_remove.discard(name) def not_cached(item): fname, field = item @@ -2090,3 +2091,42 @@ _pyson_encoder = PYSONEncoder() + + +def ModelAccessProxy(record, context=None): + pool = Pool() + ModelAccess = pool.get('ir.model.access') + FieldAccess = pool.get('ir.model.field.access') + Rule = pool.get('ir.rule') + + model = record.__class__ + with Transaction().set_context(context), check_access(): + ModelAccess.check(model.__name__) + Rule.check(model.__name__, [record.id]) + + class _ModelAccessProxy: + __class__ = model + + def __init__(self, id): + self.id = id + + def __getattr__(self, name): + if name in model._fields: + with Transaction().set_context(context), check_access(): + FieldAccess.check(model.__name__, [name]) + value = getattr(record, name) + if isinstance(value, Model): + value = ModelAccessProxy(value, context) + elif isinstance(value, (list, tuple)): + value = [ + ModelAccessProxy(r, context) + if isinstance(r, Model) else r for r in value] + return value + + def __int__(self): + return int(self.id) + + def __str__(self): + return f'{model.__name__},{self.id}' + + return _ModelAccessProxy(record.id) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/model/modelview.py new/trytond-7.0.55/trytond/model/modelview.py --- old/trytond-7.0.51/trytond/model/modelview.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/trytond/model/modelview.py 2026-07-08 08:36:10.000000000 +0200 @@ -408,10 +408,9 @@ tree_root = tree.getroottree().getroot() # Find field without read access - fread_accesses = FieldAccess.check(cls.__name__, - list(cls._fields.keys()), 'read', access=True) - fields_to_remove = set( - x for x, y in fread_accesses.items() if not y) + fields_access = FieldAccess.get_access([cls.__name__])[cls.__name__] + fields_to_remove = { + f for f, a in fields_access.items() if not a['read']} # Find relation field without read access for name, field in cls._fields.items(): @@ -631,8 +630,8 @@ button_groups = Button.get_groups(cls.__name__, button_name) if ((button_groups and not groups & button_groups) or (not button_groups - and not ModelAccess.check( - cls.__name__, 'write', raise_exception=False))): + and not ModelAccess.get_access( + [cls.__name__])[cls.__name__]['write'])): states = states.copy() states['readonly'] = True element.set('states', encoder.encode(states)) @@ -667,8 +666,8 @@ action = None if (not action or not action.res_model - or not ModelAccess.check( - action.res_model, 'read', raise_exception=False)): + or not ModelAccess.get_access( + [action.res_model])[action.res_model]['read']): element.tag = 'label' colspan = element.attrib.get('colspan') link_name = element.attrib['name'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/report/report.py new/trytond-7.0.55/trytond/report/report.py --- old/trytond-7.0.51/trytond/report/report.py 2026-02-27 18:00:45.000000000 +0100 +++ new/trytond-7.0.55/trytond/report/report.py 2026-08-04 10:40:12.000000000 +0200 @@ -386,7 +386,12 @@ if (weasyprint and input_format in {'html', 'xhtml'} and output_format == 'pdf'): - return output_format, weasyprint.HTML(string=data).write_pdf() + return output_format, weasyprint.HTML( + string=data, + url_fetcher=weasyprint.URLFetcher( + allowed_protocols={'http', 'https'}, + ), + ).write_pdf() if input_format == output_format and output_format in MIMETYPES: return output_format, data diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/res/user.py new/trytond-7.0.55/trytond/res/user.py --- old/trytond-7.0.51/trytond/res/user.py 2025-12-14 17:59:10.000000000 +0100 +++ new/trytond-7.0.55/trytond/res/user.py 2026-07-08 08:36:10.000000000 +0200 @@ -636,6 +636,10 @@ Group = pool.get('res.group') transaction = Transaction() + + if '_groups' in transaction.context: + return transaction.context['_groups'] + user = transaction.user groups = cls._get_groups_cache.get(user) if groups is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_access.py new/trytond-7.0.55/trytond/tests/test_access.py --- old/trytond-7.0.51/trytond/tests/test_access.py 2025-12-14 21:34:25.000000000 +0100 +++ new/trytond-7.0.55/trytond/tests/test_access.py 2026-07-08 08:36:10.000000000 +0200 @@ -3,6 +3,7 @@ # this repository contains the full copyright notices and license terms. import unittest +from trytond.model import ModelAccessProxy from trytond.model.exceptions import AccessError from trytond.pool import Pool from trytond.tests.test_tryton import activate_module, with_transaction @@ -404,6 +405,39 @@ with self.assertRaises(AccessError): TestAccess.search([], order=[('relate.value', 'ASC')]) + @with_transaction() + def test_model_access_proxy(self): + "Test model access proxy" + pool = Pool() + Model = pool.get('ir.model') + ModelAccess = pool.get('ir.model.access') + TestAccess = pool.get(self.model_name) + record, = TestAccess.create([{}]) + model, = Model.search([('model', '=', self.model_name)]) + ModelAccess.create([{ + 'model': model.id, + 'perm_read': True, + }]) + + ModelAccessProxy(record, {}) + + @with_transaction() + def test_model_access_proxy_no_access(self): + "Test model access proxy without access" + pool = Pool() + Model = pool.get('ir.model') + ModelAccess = pool.get('ir.model.access') + TestAccess = pool.get(self.model_name) + record, = TestAccess.create([{}]) + model, = Model.search([('model', '=', self.model_name)]) + ModelAccess.create([{ + 'model': model.id, + 'perm_read': False, + }]) + + with self.assertRaises(AccessError): + ModelAccessProxy(record, {}) + class ModelAccessWriteTestCase(_ModelAccessTestCase): _perm = 'perm_write' @@ -1105,6 +1139,49 @@ with self.assertRaises(AccessError): TestAccess.search([('relate', 'child_of', 42, 'parent')]) + @with_transaction() + def test_model_access_proxy(self): + "Test model access proxy" + pool = Pool() + Field = pool.get('ir.model.field') + FieldAccess = pool.get('ir.model.field.access') + TestAccess = pool.get('test.access') + record, = TestAccess.create([{'field1': "foo"}]) + field, = Field.search([ + ('model.model', '=', 'test.access'), + ('name', '=', 'field1'), + ]) + FieldAccess.create([{ + 'field': field.id, + 'perm_read': True, + }]) + + proxy = ModelAccessProxy(record, {}) + + self.assertEqual(proxy.field1, 'foo') + + @with_transaction() + def test_model_access_proxy_no_access(self): + "Test model access proxy without access" + pool = Pool() + Field = pool.get('ir.model.field') + FieldAccess = pool.get('ir.model.field.access') + TestAccess = pool.get('test.access') + record, = TestAccess.create([{'field1': "foo"}]) + field, = Field.search([ + ('model.model', '=', 'test.access'), + ('name', '=', 'field1'), + ]) + FieldAccess.create([{ + 'field': field.id, + 'perm_read': False, + }]) + + proxy = ModelAccessProxy(record, {}) + + with self.assertRaises(AccessError): + ModelAccessProxy(proxy.field1) + class ModelFieldAccessWriteTestCase(_ModelFieldAccessTestCase): _perm = 'perm_write' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_field_many2one.py new/trytond-7.0.55/trytond/tests/test_field_many2one.py --- old/trytond-7.0.51/trytond/tests/test_field_many2one.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/trytond/tests/test_field_many2one.py 2026-07-08 08:36:10.000000000 +0200 @@ -274,6 +274,27 @@ self.assertEqual(record.target.context, 'foo') + @with_transaction() + def test_search_where_target_union(self): + "Test search on Many2One using where and UNION-ed query on target" + pool = Pool() + Target = pool.get('test.many2one_target') + Many2One = pool.get('test.many2one') + target1, target2 = Target.create([ + {'value': 1}, + {'value': 2}, + ]) + many2one1, many2one2 = Many2One.create([ + {'many2one': target1}, + {'many2one': target2}, + ]) + + many2ones = Many2One.search([ + ('many2one', 'where', + ['OR', ('value', '=', 1), ('write_uid.id', '=', -1)]), + ]) + self.assertListEqual(many2ones, [many2one1]) + class FieldMany2OneTreeTestCase(unittest.TestCase): "Test Field Many2One Tree" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_genshi.py new/trytond-7.0.55/trytond/tests/test_genshi.py --- old/trytond-7.0.51/trytond/tests/test_genshi.py 1970-01-01 01:00:00.000000000 +0100 +++ new/trytond-7.0.55/trytond/tests/test_genshi.py 2026-07-08 08:36:10.000000000 +0200 @@ -0,0 +1,36 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +import unittest + +from genshi.template import MarkupTemplate, TextTemplate +from genshi.template.eval import UndefinedError + + +class GenshiTestCase(unittest.TestCase): + + def test_no_builtins(self): + "Test no builtins" + with self.assertRaises(UndefinedError): + str(TextTemplate("${open('%s').read()}" % __file__).generate()) + + def test_no_private_name(self): + "Test no private name" + with self.assertRaisesRegex(ValueError, r"invalid name '__import__'"): + str(TextTemplate("${__import__('os')}").generate()) + + def test_no_private_attribute(self): + "Test no private attribute" + with self.assertRaisesRegex( + ValueError, r"invalid attribute '__getattribute__'"): + str(TextTemplate("${True.__getattribute__}").generate()) + + def test_no_import(self): + "Test no import" + with self.assertRaisesRegex(ValueError, r"invalid import"): + str(MarkupTemplate("<?python import os?>").generate()) + + def test_no_import_from(self): + "Test no import from" + with self.assertRaisesRegex(ValueError, r"invalid import from"): + str(MarkupTemplate("<?python from os import popen?>").generate()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/tests/test_rule.py new/trytond-7.0.55/trytond/tests/test_rule.py --- old/trytond-7.0.51/trytond/tests/test_rule.py 2025-09-22 21:34:39.000000000 +0200 +++ new/trytond-7.0.55/trytond/tests/test_rule.py 2026-07-08 08:36:10.000000000 +0200 @@ -3,6 +3,7 @@ import json import unittest +from trytond.model import ModelAccessProxy from trytond.model.exceptions import AccessError from trytond.pool import Pool from trytond.tests.test_tryton import activate_module, with_transaction @@ -708,3 +709,52 @@ with self.assertRaisesRegex(AccessError, "Field different from foo"): TestRuleModel.read([test.id], ['name']) + + @with_transaction() + def test_model_access_proxy(self): + "Test model access proxy" + pool = Pool() + TestRule = pool.get('test.rule') + RuleGroup = pool.get('ir.rule.group') + Model = pool.get('ir.model') + + model, = Model.search([('model', '=', 'test.rule')]) + rule_group, = RuleGroup.create([{ + 'name': "Field different from foo", + 'model': model.id, + 'global_p': True, + 'perm_read': False, + 'rules': [('create', [{ + 'domain': json.dumps( + [('field', '!=', 'foo')]), + }])], + }]) + record, = TestRule.create([{'field': 'foo'}]) + + proxy = ModelAccessProxy(record, {}) + + self.assertEqual(proxy.field, 'foo') + + @with_transaction() + def test_model_access_proxy_no_access(self): + "Test model access proxy without access" + pool = Pool() + TestRule = pool.get('test.rule') + RuleGroup = pool.get('ir.rule.group') + Model = pool.get('ir.model') + + model, = Model.search([('model', '=', 'test.rule')]) + rule_group, = RuleGroup.create([{ + 'name': "Field different from foo", + 'model': model.id, + 'global_p': True, + 'perm_read': True, + 'rules': [('create', [{ + 'domain': json.dumps( + [('field', '!=', 'foo')]), + }])], + }]) + record, = TestRule.create([{'field': 'foo'}]) + + with self.assertRaises(AccessError): + ModelAccessProxy(record, {}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/tools/misc.py new/trytond-7.0.55/trytond/tools/misc.py --- old/trytond-7.0.51/trytond/tools/misc.py 2026-04-14 20:36:56.000000000 +0200 +++ new/trytond-7.0.55/trytond/tools/misc.py 2026-07-08 08:36:10.000000000 +0200 @@ -210,8 +210,15 @@ def resolve(name): "Resolve a dotted name to a global object." name = name.split('.') - used = name.pop(0) - found = importlib.import_module(used) + if name[:2] == ['trytond', 'modules']: + # import the Tryton module using entry point + name = name[2:] + used = name.pop(0) + found = import_module(used) + used = found.__name__ + else: + used = name.pop(0) + found = importlib.import_module(used) for n in name: used = used + '.' + n try: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond/transaction.py new/trytond-7.0.55/trytond/transaction.py --- old/trytond-7.0.51/trytond/transaction.py 2026-04-30 21:39:01.000000000 +0200 +++ new/trytond-7.0.55/trytond/transaction.py 2026-07-08 08:36:10.000000000 +0200 @@ -406,7 +406,9 @@ @property def check_access(self): - return self.context.get('_check_access', False) + return ( + self.context.get('_check_access', False) + and self.user != 0) @property def active_records(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond.egg-info/PKG-INFO new/trytond-7.0.55/trytond.egg-info/PKG-INFO --- old/trytond-7.0.51/trytond.egg-info/PKG-INFO 2026-06-02 18:46:50.000000000 +0200 +++ new/trytond-7.0.55/trytond.egg-info/PKG-INFO 2026-08-04 12:06:01.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: trytond -Version: 7.0.51 +Version: 7.0.55 Summary: Tryton server Home-page: http://www.tryton.org/ Download-URL: http://downloads.tryton.org/7.0/ @@ -77,7 +77,7 @@ Provides-Extra: html2text Requires-Dist: html2text; extra == "html2text" Provides-Extra: weasyprint -Requires-Dist: weasyprint; extra == "weasyprint" +Requires-Dist: weasyprint>=68.0; extra == "weasyprint" Provides-Extra: coroutine Requires-Dist: gevent>=1.1; extra == "coroutine" Provides-Extra: image diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond.egg-info/SOURCES.txt new/trytond-7.0.55/trytond.egg-info/SOURCES.txt --- old/trytond-7.0.51/trytond.egg-info/SOURCES.txt 2026-06-02 18:46:50.000000000 +0200 +++ new/trytond-7.0.55/trytond.egg-info/SOURCES.txt 2026-08-04 12:06:01.000000000 +0200 @@ -91,6 +91,7 @@ doc/tutorial/module/wizard.rst doc/tutorial/module/workflow.rst trytond/__init__.py +trytond/_safe_genshi.py trytond/admin.py trytond/application.py trytond/bus.py @@ -507,6 +508,7 @@ trytond/tests/test_field_time.py trytond/tests/test_field_timedelta.py trytond/tests/test_filestore.py +trytond/tests/test_genshi.py trytond/tests/test_history.py trytond/tests/test_i18n.py trytond/tests/test_importdata.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trytond-7.0.51/trytond.egg-info/requires.txt new/trytond-7.0.55/trytond.egg-info/requires.txt --- old/trytond-7.0.51/trytond.egg-info/requires.txt 2026-06-02 18:46:50.000000000 +0200 +++ new/trytond-7.0.55/trytond.egg-info/requires.txt 2026-08-04 12:06:01.000000000 +0200 @@ -56,4 +56,4 @@ pillow [weasyprint] -weasyprint +weasyprint>=68.0
