• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Zope / 3956162881

pending completion
3956162881

push

github

Michael Howitz
Update to deprecation warning free releases.

4401 of 7036 branches covered (62.55%)

Branch coverage included in aggregate %.

27161 of 31488 relevant lines covered (86.26%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.95
/src/Products/PageTemplates/engine.py
1
"""``chameleon`` integration.
2

3
The engine returned  by the template's ``pt_getEngine`` decides
4
whether the ``zope.tales`` or
5
the ``chameleon.tales`` TALES implementation is used:
6
``zope.tales`` is used when the engine is an instance of
7
``zope.pagetemplate.enging.ZopeBaseEngine``,
8
``chameleon.tales`` otherwise. This could get more flexible
9
in the future.
10
"""
11

12
import ast
1✔
13
import logging
1✔
14
import re
1✔
15
from collections.abc import Mapping
1✔
16
from weakref import ref
1✔
17

18
from chameleon.astutil import Static
1✔
19
from chameleon.astutil import Symbol
1✔
20
from chameleon.codegen import template
1✔
21
from chameleon.exc import ExpressionError
1✔
22
from chameleon.tal import RepeatDict
1✔
23
from chameleon.tales import DEFAULT_MARKER  # only in chameleon 3.8.0 and up
1✔
24
from chameleon.zpt.template import Macros
1✔
25

26
from AccessControl.class_init import InitializeClass
1✔
27
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
28
from App.version_txt import getZopeVersion
1✔
29
from MultiMapping import MultiMapping
1✔
30
from z3c.pt.pagetemplate import PageTemplate as ChameleonPageTemplate
1✔
31
from zope.interface import implementer
1✔
32
from zope.interface import provider
1✔
33
from zope.pagetemplate.engine import ZopeBaseEngine
1✔
34
from zope.pagetemplate.interfaces import IPageTemplateEngine
1✔
35
from zope.pagetemplate.interfaces import IPageTemplateProgram
1✔
36
from zope.tales.expressions import PathExpr
1✔
37
from zope.tales.expressions import SubPathExpr
1✔
38

39
from .Expressions import PathIterator
1✔
40
from .Expressions import SecureModuleImporter
1✔
41
from .interfaces import IZopeAwareEngine
1✔
42

43

44
class _PseudoContext:
1✔
45
    """auxiliary context object.
46

47
    Used to bridge between ``chameleon`` and ``zope.tales`` iterators.
48
    """
49
    @staticmethod
1✔
50
    def setLocal(*args):
1✔
51
        pass
1✔
52

53

54
class RepeatDictWrapper(RepeatDict):
1✔
55
    """Wrap ``chameleon``s ``RepeatDict``.
56

57
    Aims:
58

59
      1. make it safely usable by untrusted code
60

61
      2. let it use a ``zope.tales`` compatible ``RepeatItem``
62
    """
63

64
    security = ClassSecurityInfo()
1✔
65
    security.declareObjectPublic()
1✔
66
    security.declarePrivate(  # NOQA: D001
1✔
67
        *(set(dir(RepeatDict)) - set(dir(MultiMapping))))  # NOQA: D001
68
    __allow_access_to_unprotected_subobjects__ = True
1✔
69

70
    # Logic (mostly) from ``chameleon.tal.RepeatDict``
71
    def __call__(self, key, iterable):
1✔
72
        """We coerce the iterable to a tuple and return an iterator
73
        after registering it in the repeat dictionary."""
74
        iterable = list(iterable) if iterable is not None else ()
1✔
75

76
        length = len(iterable)
1✔
77

78
        # Insert as repeat item
79
        ri = self[key] = RepeatItem(None, iterable, _PseudoContext)
1✔
80

81
        return ri, length
1✔
82

83

84
InitializeClass(RepeatDictWrapper)
1✔
85

86

87
class RepeatItem(PathIterator):
1✔
88
    """Iterator compatible with ``chameleon`` and ``zope.tales``."""
89
    def __iter__(self):
1✔
90
        return self
1✔
91

92
    def __next__(self):
1✔
93
        if super().__next__():
1✔
94
            return self.item
1✔
95
        else:
96
            raise StopIteration
1✔
97

98

99
# Declare Chameleon Macros object accessible
100
# This makes subscripting work, as in template.macros['name']
101
Macros.__allow_access_to_unprotected_subobjects__ = True
1✔
102

103
re_match_pi = re.compile(r'<\?python([^\w].*?)\?>', re.DOTALL)
1✔
104
logger = logging.getLogger('Products.PageTemplates')
1✔
105

106

107
# zt_expr registry management
108
#  ``chameleon`` compiles TALES expressions to Python byte code
109
#  and includes it in the byte code generated for the complete
110
#  template. In production mode, the whole is cached
111
#  in a long term (across process invocations) file system based
112
#  cache, keyed with the digest of the template's source and
113
#  some names.
114
#  ``zope.tales`` expressions are essentially runtime objects
115
#  (general "callables", usually class instances with a ``__call__``
116
#  method. They cannot be (easily) represented via byte code.
117
#  We address this problem by representing such an expression
118
#  in the byte code as a function call to compile the expression.
119
#  For efficiency, a (process local) compile cache is used
120
#  to cache compilation results, the ``zt_expr_registry``,
121
#  keyed by the engine id, the expression type and the expression source.
122

123
_zt_expr_registry = {}
1✔
124

125

126
def _compile_zt_expr(type, expression, engine=None, econtext=None):
1✔
127
    """compile *expression* of type *type*.
128

129
    The engine is derived either directly from *engine* or the
130
    execution content *econtext*. One of them must be given.
131

132
    The compilation result is cached in ``_zt_expr_registry``.
133
    """
134
    if engine is None:
1✔
135
        engine = econtext["__zt_engine__"]
1✔
136
    key = id(engine), type, expression
1✔
137
    # cache lookup does not need to be protected by locking
138
    #  (but we could potentially prevent unnecessary computations)
139
    expr = _zt_expr_registry.get(key)
1✔
140
    if expr is None:
1✔
141
        expr = engine.types[type](type, expression, engine)
1✔
142
        _zt_expr_registry[key] = expr
1✔
143
    return expr
1✔
144

145

146
_compile_zt_expr_node = Static(Symbol(_compile_zt_expr))
1✔
147

148

149
# map context class to context wrapper class
150
_context_class_registry = {}
1✔
151

152

153
def _with_vars_from_chameleon(context):
1✔
154
    """prepare *context* to get its ``vars`` from ``chameleon``."""
155
    cc = context.__class__
1✔
156
    wc = _context_class_registry.get(cc)
1✔
157
    if wc is None:
1✔
158
        class ContextWrapper(_C2ZContextWrapperBase, cc):
1✔
159
            pass
1✔
160

161
        wc = _context_class_registry[cc] = ContextWrapper
1✔
162
    context.__class__ = wc
1✔
163
    return ref(context)
1✔
164

165

166
class Name2KeyError(Mapping):
1✔
167
    # auxiliary class to convert ``chameleon``'s ``NameError``
168
    # into ``KeyError``
169
    def __init__(self, mapping):
1✔
170
        self.data = mapping
1✔
171

172
    def __getitem__(self, key):
1✔
173
        try:
1✔
174
            return self.data[key]
1✔
175
        except NameError:
1✔
176
            raise KeyError(key)
×
177

178
    def __iter__(self):
1✔
179
        return iter(self.data)
1✔
180

181
    def __len__(self):
1✔
182
        return len(self.data)
1✔
183

184

185
class _C2ZContextWrapperBase:
1✔
186
    """Behaves like "zope" context with vars from "chameleon" context.
187

188
    It is assumed that an instance holds the current ``chameleon``
189
    context in its attribute ``_c_context``.
190
    """
191
    @property
1✔
192
    def vars(self):
1✔
193
        return Name2KeyError(self._c_context)
1✔
194

195
    # delegate to `_c_context`
196
    def getValue(self, name, default=None):
1✔
197
        try:
1✔
198
            return self._c_context[name]
1✔
199
        except (NameError, KeyError):
1✔
200
            return default
1✔
201

202
    get = getValue
1✔
203

204
    def setLocal(self, name, value):
1✔
205
        self._c_context.setLocal(name, value)
1✔
206

207
    def setGlobal(self, name, value):
1✔
208
        self._c_context.setGlobal(name, value)
1✔
209

210
    # unsupported methods
211
    def beginScope(self, *args, **kw):
1✔
212
        """will not work as the scope is controlled by ``chameleon``."""
213
        raise NotImplementedError()
214

215
    endScope = beginScope
1✔
216
    setSourceFile = beginScope
1✔
217
    setPosition = beginScope
1✔
218
    setRepeat = beginScope
1✔
219

220
    # work around bug in ``zope.tales.tales.Context``
221
    def getDefault(self):
1✔
222
        return self.contexts["default"]
1✔
223

224

225
def _C2ZContextWrapper(c_context, attrs):
1✔
226
    c_context["attrs"] = attrs
1✔
227
    zt_context = c_context["__zt_context__"]()
1✔
228
    zt_context._c_context = c_context
1✔
229
    return zt_context
1✔
230

231

232
_c_context_2_z_context_node = Static(Symbol(_C2ZContextWrapper))
1✔
233

234

235
# exclude characters special for ``chameleon``'s interpolation syntax
236
# from use in path expressions to reduce the failure risk
237
# for the ``chameleon`` interpolation heuristics
238
BAD_PATH_CHARS = "${}"
1✔
239
contains_bad_path_chars = re.compile("[%s]" % BAD_PATH_CHARS).search
1✔
240

241

242
class MappedExpr:
1✔
243
    """map expression: ``zope.tales`` --> ``chameleon.tales``."""
244
    def __init__(self, type, expression, zt_engine):
1✔
245
        self.type = type
1✔
246
        # At this place, *expression* is a `chameleon.tokenize.Token`
247
        # (a subtype of `str` for PY3 and of `unicode` for PY2).
248
        # The ``_compile_zt_expr`` below causes this to be cached
249
        # which can lead under Python 3 to unsolicited translations
250
        # (details "https://github.com/zopefoundation/Zope/issues/876")
251
        # To avoid this, call ``_compile_zt_expr`` with
252
        # *expression* cast to the `Token` base type.
253
        expr = str(expression)
1✔
254
        self.expression = expression
1✔
255
        # compile to be able to report errors
256
        compiler_error = zt_engine.getCompilerError()
1✔
257
        try:
1✔
258
            zt_expr = _compile_zt_expr(type, expr, engine=zt_engine)
1✔
259
        except compiler_error as e:
1✔
260
            raise ExpressionError(str(e), self.expression)
1✔
261
        if (self.type == "path"
1✔
262
                and isinstance(zt_expr, PathExpr)
263
                and contains_bad_path_chars(self.expression)):
264
            # the ``chameleon`` template engine has a really curious
265
            #   implementation of global ``$`` interpolation
266
            #   (see ``chameleon.compiler.Interpolator``):
267
            #   when it sees ``${``, it starts with the largest
268
            #   substring starting at this position and ending in ``}``
269
            #   and tries to generate code for it. If this fails, it
270
            #   retries with the second largest such substring, etc.
271
            # Of course, this fails with ``zope.tales`` ``path`` expressions
272
            #   where almost any character is syntactically legal.
273
            #   Thus, it happily generates code for e.g.
274
            #   ``d/a} ${d/b`` (resulting from ``${d/a} ${d/b}``)
275
            #   but its evaluation will fail (with high likelyhood).
276
            # We use a heuristics here to handle many (but not all)
277
            #   resulting problems: forbid special characters
278
            #   for interpolation in ``SubPathExpr``s.
279
            for se in zt_expr._subexprs:
1!
280
                # dereference potential evaluation method
281
                se = getattr(se, "__self__", se)
1✔
282
                # we assume below that expressions other than
283
                # ``SubPathExpr`` have flagged out use of the special
284
                # characters already
285
                # we know that this assumption is wrong in some cases
286
                if isinstance(se, SubPathExpr):
1!
287
                    for pe in se._compiled_path:
1!
288
                        if isinstance(pe, tuple):  # standard path
1!
289
                            for spe in pe:
1!
290
                                if contains_bad_path_chars(spe):
1!
291
                                    raise ExpressionError(
1✔
292
                                        "%s unsupported",
293
                                        BAD_PATH_CHARS)
294

295
    def __call__(self, target, c_engine):
1✔
296
        # The convoluted handling of ``attrs`` below was necessary
297
        # for some ``chameleon`` versions to work around
298
        # "https://github.com/malthe/chameleon/issues/323".
299
        # The work round is partial only: until the ``chameleon``
300
        # problem is fixed, `attrs` cannot be used inside ``tal:define``.
301
        # Potentially, ``attrs`` handling could be simplified
302
        # for ``chameleon > 3.8.0``.
303
        return template(
1✔
304
            "try: __zt_tmp = attrs\n"
305
            "except NameError: __zt_tmp = None\n"
306
            "target = compile_zt_expr(type, expression, econtext=econtext)"
307
            "(c2z_context(econtext, __zt_tmp))",
308
            target=target,
309
            compile_zt_expr=_compile_zt_expr_node,
310
            type=ast.Str(self.type),
311
            expression=ast.Str(self.expression),
312
            c2z_context=_c_context_2_z_context_node,
313
            attrs=ast.Name("attrs", ast.Load()))
314

315

316
class MappedExprType:
1✔
317
    """map expression type: ``zope.tales`` --> ``chameleon.tales``."""
318
    def __init__(self, engine, type):
1✔
319
        self.engine = engine
1✔
320
        self.type = type
1✔
321

322
    def __call__(self, expression):
1✔
323
        return MappedExpr(self.type, expression, self.engine)
1✔
324

325

326
class ZtPageTemplate(ChameleonPageTemplate):
1✔
327
    """``ChameleonPageTemplate`` using ``zope.tales.tales._default``.
328

329
    Note: this is not necessary when ``chameleon.tales`` is used
330
    but it does not hurt to use the fixed value to represent ``default``
331
    rather than a template specific value.
332
    """
333

334
    # use `chameleon` configuration to obtain more
335
    # informative error information
336
    value_repr = staticmethod(repr)
1✔
337

338

339
@implementer(IPageTemplateProgram)
1✔
340
@provider(IPageTemplateEngine)
1✔
341
class Program:
1✔
342

343
    def __init__(self, template, engine):
1✔
344
        self.template = template
1✔
345
        self.engine = IZopeAwareEngine(engine, engine)
1✔
346

347
    def __call__(self, context, macros, tal=True, **options):
1✔
348
        if tal is False:
1✔
349
            return self.template.body
1✔
350

351
        # Swap out repeat dictionary for Chameleon implementation
352
        kwargs = context.vars
1✔
353
        kwargs['repeat'] = RepeatDictWrapper(context.repeat_vars)
1✔
354
        # provide context for ``zope.tales`` expression compilation
355
        #   and evaluation
356
        #   unused for ``chameleon.tales`` expressions
357
        kwargs["__zt_engine__"] = self.engine
1✔
358
        kwargs["__zt_context__"] = _with_vars_from_chameleon(context)
1✔
359

360
        template = self.template
1✔
361
        # ensure ``chameleon`` ``default`` representation
362
        context.setContext("default", DEFAULT_MARKER)
1✔
363
        kwargs["default"] = DEFAULT_MARKER
1✔
364

365
        return template.render(**kwargs)
1✔
366

367
    @classmethod
1✔
368
    def cook(cls, source_file, text, engine, content_type):
1✔
369
        if getattr(engine, "untrusted", False):
1✔
370
            def sanitize(m):
1✔
371
                match = m.group(1)
1✔
372
                logger.info(
1✔
373
                    'skipped "<?python%s?>" code block in '
374
                    'Zope 2 page template object "%s".',
375
                    match, source_file
376
                )
377
                return ''
1✔
378

379
            text, count = re_match_pi.subn(sanitize, text)
1✔
380
            if count:
1✔
381
                logger.warning(
1✔
382
                    "skipped %d code block%s (not allowed in "
383
                    "restricted evaluation scope)." % (
384
                        count, 's' if count > 1 else ''
385
                    )
386
                )
387

388
        if isinstance(engine, ZopeBaseEngine):
1✔
389
            # use ``zope.tales`` expressions
390
            expr_types = {ty: MappedExprType(engine, ty)
1✔
391
                          for ty in engine.types}
392
        else:
393
            # use ``chameleon.tales`` expressions
394
            expr_types = engine.types
1✔
395

396
        # BBB: Support CMFCore's FSPagetemplateFile formatting
397
        if source_file is not None and source_file.startswith('file:'):
1!
398
            source_file = source_file[5:]
×
399

400
        if source_file is None:
1✔
401
            # Default to '<string>'
402
            source_file = ChameleonPageTemplate.filename
1✔
403

404
        zope_version = getZopeVersion()
1✔
405
        template = ZtPageTemplate(
1✔
406
            text, filename=source_file, keep_body=True,
407
            expression_types=expr_types,
408
            encoding='utf-8',
409
            extra_builtins={
410
                "modules": SecureModuleImporter,
411
                # effectively invalidate the template file cache for
412
                #   every new ``Zope`` version
413
                "zope_version_" + "_".join(
414
                    str(c) for c in zope_version
415
                    if not (isinstance(c, int) and c < 0)): getZopeVersion}
416
        )
417

418
        return cls(template, engine), template.macros
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc