[FIX] AttributeError, 'module' has no attribute 'ext_eval'
[odoo/odoo.git] / openerp / 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
34 from opcode import HAVE_ARGUMENT, opmap, opname
35 from types import CodeType
36 import logging
37 import os
38
39 __all__ = ['test_expr', 'safe_eval', 'const_eval']
40
41 # The time module is usually already provided in the safe_eval environment
42 # but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
43 # lp:703841), does import time.
44 _ALLOWED_MODULES = ['_strptime', 'time']
45
46 _CONST_OPCODES = set(opmap[x] for x in [
47     'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', 'DUP_TOPX',
48     'POP_BLOCK','SETUP_LOOP', 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
49     'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
50
51 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
52     'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
53     'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
54     'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
55     'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
56     'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
57     'BINARY_OR', 'INPLACE_ADD', 'INPLACE_SUBTRACT', 'INPLACE_MULTIPLY',
58     'INPLACE_DIVIDE', 'INPLACE_REMAINDER', 'INPLACE_POWER',
59     'INPLACE_LEFTSHIFT', 'INPLACE_RIGHTSHIFT', 'INPLACE_AND',
60     'INPLACE_XOR','INPLACE_OR'
61     ] if x in opmap))
62
63 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
64     'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
65     'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
66     'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
67     'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
68     # New in Python 2.7 - http://bugs.python.org/issue4715 :
69     'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
70     'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY'
71     ] if x in opmap))
72
73 _logger = logging.getLogger('safe_eval')
74
75 def _get_opcodes(codeobj):
76     """_get_opcodes(codeobj) -> [opcodes]
77
78     Extract the actual opcodes as a list from a code object
79
80     >>> c = compile("[1 + 2, (1,2)]", "", "eval")
81     >>> _get_opcodes(c)
82     [100, 100, 23, 100, 100, 102, 103, 83]
83     """
84     i = 0
85     opcodes = []
86     byte_codes = codeobj.co_code
87     while i < len(byte_codes):
88         code = ord(byte_codes[i])
89         opcodes.append(code)
90         if code >= HAVE_ARGUMENT:
91             i += 3
92         else:
93             i += 1
94     return opcodes
95
96 def test_expr(expr, allowed_codes, mode="eval"):
97     """test_expr(expression, allowed_codes[, mode]) -> code_object
98
99     Test that the expression contains only the allowed opcodes.
100     If the expression is valid and contains only allowed codes,
101     return the compiled code object.
102     Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
103     """
104     try:
105         if mode == 'eval':
106             # eval() does not like leading/trailing whitespace
107             expr = expr.strip()
108         code_obj = compile(expr, "", mode)
109     except (SyntaxError, TypeError):
110         _logger.debug('Invalid eval expression', exc_info=True)
111         raise
112     except Exception:
113         _logger.debug('Disallowed or invalid eval expression', exc_info=True)
114         raise ValueError("%s is not a valid expression" % expr)
115     for code in _get_opcodes(code_obj):
116         if code not in allowed_codes:
117             raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
118     return code_obj
119
120
121 def const_eval(expr):
122     """const_eval(expression) -> value
123
124     Safe Python constant evaluation
125
126     Evaluates a string that contains an expression describing
127     a Python constant. Strings that are not valid Python expressions
128     or that contain other code besides the constant raise ValueError.
129
130     >>> const_eval("10")
131     10
132     >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
133     [1, 2, (3, 4), {'foo': 'bar'}]
134     >>> const_eval("1+2")
135     Traceback (most recent call last):
136     ...
137     ValueError: opcode BINARY_ADD not allowed
138     """
139     c = test_expr(expr, _CONST_OPCODES)
140     return eval(c)
141
142 def expr_eval(expr):
143     """expr_eval(expression) -> value
144
145     Restricted Python expression evaluation
146
147     Evaluates a string that contains an expression that only
148     uses Python constants. This can be used to e.g. evaluate
149     a numerical expression from an untrusted source.
150
151     >>> expr_eval("1+2")
152     3
153     >>> expr_eval("[1,2]*2")
154     [1, 2, 1, 2]
155     >>> expr_eval("__import__('sys').modules")
156     Traceback (most recent call last):
157     ...
158     ValueError: opcode LOAD_NAME not allowed
159     """
160     c = test_expr(expr, _EXPR_OPCODES)
161     return eval(c)
162
163 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
164     if globals is None:
165         globals = {}
166     if locals is None:
167         locals = {}
168     if fromlist is None:
169         fromlist = []
170     if name in _ALLOWED_MODULES:
171         return __import__(name, globals, locals, level)
172     raise ImportError(name)
173
174 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
175     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
176
177     System-restricted Python expression evaluation
178
179     Evaluates a string that contains an expression that mostly
180     uses Python constants, arithmetic expressions and the
181     objects directly provided in context.
182
183     This can be used to e.g. evaluate
184     an OpenERP domain expression from an untrusted source.
185
186     Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
187
188     >>> safe_eval("__import__('sys').modules")
189     Traceback (most recent call last):
190     ...
191     ValueError: opcode LOAD_NAME not allowed
192
193     """
194     if isinstance(expr, CodeType):
195         raise ValueError("safe_eval does not allow direct evaluation of code objects.")
196
197     if '__subclasses__' in expr:
198        raise ValueError('expression not allowed (__subclasses__)')
199
200     if globals_dict is None:
201         globals_dict = {}
202
203     # prevent altering the globals/locals from within the sandbox
204     # by taking a copy.
205     if not nocopy:
206         # isinstance() does not work below, we want *exactly* the dict class
207         if (globals_dict is not None and type(globals_dict) is not dict) \
208             or (locals_dict is not None and type(locals_dict) is not dict):
209             logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
210                               "you should probably pass nocopy=True to safe_eval()')
211
212         globals_dict = dict(globals_dict)
213         if locals_dict is not None:
214             locals_dict = dict(locals_dict)
215
216     globals_dict.update(
217             __builtins__ = {
218                 '__import__': _import,
219                 'True': True,
220                 'False': False,
221                 'None': None,
222                 'str': str,
223                 'globals': locals,
224                 'locals': locals,
225                 'bool': bool,
226                 'dict': dict,
227                 'list': list,
228                 'tuple': tuple,
229                 'map': map,
230                 'abs': abs,
231                 'min': min,
232                 'max': max,
233                 'reduce': reduce,
234                 'filter': filter,
235                 'round': round,
236                 'len': len,
237                 'set' : set
238             }
239     )
240     return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
241
242 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: