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