rml2pdf: fix logging of exceptions.
[odoo/odoo.git] / bin / tools / safe_eval.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    Copyright (C) 2004-2010 OpenERP s.a. (<http://www.openerp.com>).
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU Affero General Public License as
7 #    published by the Free Software Foundation, either version 3 of the
8 #    License, or (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU Affero General Public License for more details.
14 #
15 #    You should have received a copy of the GNU Affero General Public License
16 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 ##############################################################################
19
20 """
21 safe_eval module - methods intended to provide more restricted alternatives to
22                    evaluate simple and/or untrusted code.
23
24 Methods in this module are typically used as alternatives to eval() to parse
25 OpenERP domain strings, conditions and expressions, mostly based on locals
26 condition/math builtins.
27 """
28
29 # Module partially ripped from/inspired by several different sources:
30 #  - http://code.activestate.com/recipes/286134/
31 #  - safe_eval in lp:~xrg/openobject-server/optimize-5.0
32 #  - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad
33 #  - python 2.6's ast.literal_eval
34
35 from opcode import HAVE_ARGUMENT, opmap, opname
36 from types import CodeType
37 import logging
38 import os
39
40 __all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ]
41
42 _CONST_OPCODES = set(opmap[x] for x in [
43     'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
44     'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
45     'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
46
47 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
48     'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
49     'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
50     'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
51     'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
52     'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
53     'BINARY_OR'] if x in opmap))
54
55 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
56     'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
57     'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
58     'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
59     'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
60     # New in Python 2.7 - http://bugs.python.org/issue4715 :
61     'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
62     'POP_JUMP_IF_TRUE'
63     ] if x in opmap))
64
65 _logger = logging.getLogger('safe_eval')
66
67 def _get_opcodes(codeobj):
68     """_get_opcodes(codeobj) -> [opcodes]
69
70     Extract the actual opcodes as a list from a code object
71
72     >>> c = compile("[1 + 2, (1,2)]", "", "eval")
73     >>> _get_opcodes(c)
74     [100, 100, 23, 100, 100, 102, 103, 83]
75     """
76     i = 0
77     opcodes = []
78     byte_codes = codeobj.co_code
79     while i < len(byte_codes):
80         code = ord(byte_codes[i])
81         opcodes.append(code)
82         if code >= HAVE_ARGUMENT:
83             i += 3
84         else:
85             i += 1
86     return opcodes
87
88 def test_expr(expr, allowed_codes, mode="eval"):
89     """test_expr(expression, allowed_codes[, mode]) -> code_object
90
91     Test that the expression contains only the allowed opcodes.
92     If the expression is valid and contains only allowed codes,
93     return the compiled code object.
94     Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
95     """
96     try:
97         if mode == 'eval':
98             # eval() does not like leading/trailing whitespace
99             expr = expr.strip()
100         code_obj = compile(expr, "", mode)
101     except (SyntaxError, TypeError):
102         _logger.debug('Invalid eval expression', exc_info=True)
103         raise
104     except Exception:
105         _logger.debug('Disallowed or invalid eval expression', exc_info=True)
106         raise ValueError("%s is not a valid expression" % expr)
107     for code in _get_opcodes(code_obj):
108         if code not in allowed_codes:
109             raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
110     return code_obj
111
112
113 def const_eval(expr):
114     """const_eval(expression) -> value
115
116     Safe Python constant evaluation
117
118     Evaluates a string that contains an expression describing
119     a Python constant. Strings that are not valid Python expressions
120     or that contain other code besides the constant raise ValueError.
121
122     >>> const_eval("10")
123     10
124     >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
125     [1, 2, (3, 4), {'foo': 'bar'}]
126     >>> const_eval("1+2")
127     Traceback (most recent call last):
128     ...
129     ValueError: opcode BINARY_ADD not allowed
130     """
131     c = test_expr(expr, _CONST_OPCODES)
132     return eval(c)
133
134 def expr_eval(expr):
135     """expr_eval(expression) -> value
136
137     Restricted Python expression evaluation
138
139     Evaluates a string that contains an expression that only
140     uses Python constants. This can be used to e.g. evaluate
141     a numerical expression from an untrusted source.
142
143     >>> expr_eval("1+2")
144     3
145     >>> expr_eval("[1,2]*2")
146     [1, 2, 1, 2]
147     >>> expr_eval("__import__('sys').modules")
148     Traceback (most recent call last):
149     ...
150     ValueError: opcode LOAD_NAME not allowed
151     """
152     c = test_expr(expr, _EXPR_OPCODES)
153     return eval(c)
154
155
156 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
157 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
158
159 try:
160     # first, try importing directly
161     from ast import literal_eval
162 except ImportError:
163     import _ast as ast
164
165     def _convert(node):
166         if isinstance(node, ast.Str):
167             return node.s
168         elif isinstance(node, ast.Num):
169             return node.n
170         elif isinstance(node, ast.Tuple):
171             return tuple(map(_convert, node.elts))
172         elif isinstance(node, ast.List):
173             return list(map(_convert, node.elts))
174         elif isinstance(node, ast.Dict):
175             return dict((_convert(k), _convert(v)) for k, v
176                         in zip(node.keys, node.values))
177         elif isinstance(node, ast.Name):
178             if node.id in SAFE_CONSTANTS:
179                 return SAFE_CONSTANTS[node.id]
180         raise ValueError('malformed or disallowed expression')
181
182     def parse(expr, filename='<unknown>', mode='eval'):
183         """parse(source[, filename], mode]] -> code object
184         Parse an expression into an AST node.
185         Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
186         """
187         return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
188
189     def literal_eval(node_or_string):
190         """literal_eval(expression) -> value
191         Safely evaluate an expression node or a string containing a Python
192         expression.  The string or node provided may only consist of the
193         following Python literal structures: strings, numbers, tuples,
194         lists, dicts, booleans, and None.
195
196         >>> literal_eval('[1,True,"spam"]')
197         [1, True, 'spam']
198
199         >>> literal_eval('1+3')
200         Traceback (most recent call last):
201         ...
202         ValueError: malformed or disallowed expression
203         """
204         if isinstance(node_or_string, basestring):
205             node_or_string = parse(node_or_string)
206         if isinstance(node_or_string, ast.Expression):
207             node_or_string = node_or_string.body
208         return _convert(node_or_string)
209
210
211
212 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
213     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
214
215     System-restricted Python expression evaluation
216
217     Evaluates a string that contains an expression that mostly
218     uses Python constants, arithmetic expressions and the
219     objects directly provided in context.
220
221     This can be used to e.g. evaluate
222     an OpenERP domain expression from an untrusted source.
223
224     Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
225
226     >>> safe_eval("__import__('sys').modules")
227     Traceback (most recent call last):
228     ...
229     ValueError: opcode LOAD_NAME not allowed
230
231     """
232     if isinstance(expr, CodeType):
233         raise ValueError("safe_eval does not allow direct evaluation of code objects.")
234
235     if '__subclasses__' in expr:
236        raise ValueError('expression not allowed (__subclasses__)')
237
238     if globals_dict is None:
239         globals_dict = {}
240
241     # prevent altering the globals/locals from within the sandbox
242     # by taking a copy.
243     if not nocopy:
244         # isinstance() does not work below, we want *exactly* the dict class
245         if (globals_dict is not None and type(globals_dict) is not dict) \
246             or (locals_dict is not None and type(locals_dict) is not dict):
247             logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
248                               "you should probably pass nocopy=True to safe_eval()')
249
250         globals_dict = dict(globals_dict)
251         if locals_dict is not None:
252             locals_dict = dict(locals_dict)
253
254     globals_dict.update(
255             __builtins__ = {
256                 'True': True,
257                 'False': False,
258                 'None': None,
259                 'str': str,
260                 'globals': locals,
261                 'locals': locals,
262                 'bool': bool,
263                 'dict': dict,
264                 'list': list,
265                 'tuple': tuple,
266                 'map': map,
267                 'abs': abs,
268                 'reduce': reduce,
269                 'filter': filter,
270                 'round': round,
271                 'len': len,
272                 'set' : set
273             }
274     )
275     return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
276
277 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: