Commit 33abe54e by Ned Batchelder

Work in progress to sandbox the uses of eval in LMS.

parent 0a6761c9
......@@ -22,7 +22,6 @@ import numpy
import os
import random
import re
import scipy
import struct
import sys
......@@ -30,6 +29,7 @@ from lxml import etree
from xml.sax.saxutils import unescape
from copy import deepcopy
<<<<<<< HEAD
import chem
import chem.miller
import chem.chemcalc
......@@ -38,8 +38,9 @@ import verifiers
import verifiers.draganddrop
import calc
=======
>>>>>>> Work in progress to sandbox the uses of eval in LMS.
from .correctmap import CorrectMap
import eia
import inputtypes
import customrender
from .util import contextualize_text, convert_files_to_filenames
......@@ -48,6 +49,8 @@ import xqueue_interface
# to be replaced with auto-registering
import responsetypes
from codejail.safe_exec import safe_exec
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
......@@ -63,6 +66,7 @@ html_transforms = {'problem': {'tag': 'div'},
"math": {'tag': 'span'},
}
<<<<<<< HEAD
global_context = {'random': random,
'numpy': numpy,
'math': math,
......@@ -73,6 +77,20 @@ global_context = {'random': random,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop}
=======
safe_exec_assumed_imports = [
"random",
"numpy",
"math",
"scipy",
"calc",
"eia",
("chemcalc", "chem.chemcalc"),
("chemtools", "chem.chemtools"),
("miller", "chem.miller"),
("draganddrop", "verifiers.draganddrop"),
]
>>>>>>> Work in progress to sandbox the uses of eval in LMS.
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
......@@ -144,7 +162,7 @@ class LoncapaProblem(object):
self._process_includes()
# construct script processor context (eg for customresponse problems)
self.context = self._extract_context(self.tree, seed=self.seed)
self.context = self._extract_context(self.tree)
# Pre-parse the XML tree: modifies it to add ID's and perform some in-place
# transformations. This also creates the dict (self.responders) of Response
......@@ -451,7 +469,7 @@ class LoncapaProblem(object):
return path
def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
def _extract_context(self, tree):
'''
Extract content of <script>...</script> from the problem.xml file, and exec it in the
context of this problem. Provides ability to randomize problems, and also set
......@@ -460,14 +478,18 @@ class LoncapaProblem(object):
Problem XML goes to Python execution context. Runs everything in script tags.
'''
random.seed(self.seed)
# save global context in here also
context = {'global_context': global_context}
# initialize context to have stuff in global_context
context.update(global_context)
# TODO: REMOVE THIS COMMENTED OUT CODE.
## save global context in here also
#context = {'global_context': global_context}
#
## initialize context to have stuff in global_context
#context.update(global_context)
#
# put globals there also
context['__builtins__'] = globals()['__builtins__']
#context['__builtins__'] = globals()['__builtins__']
context = {}
# pass instance of LoncapaProblem in
context['the_lcp'] = self
......@@ -501,7 +523,7 @@ class LoncapaProblem(object):
context['script_code'] += code
try:
# use "context" for global context; thus defs in code are global within code
exec code in context, context
safe_exec(code, context, future_division=True, assumed_imports=safe_exec_assumed_imports)
except Exception as err:
log.exception("Error while execing script code: " + code)
msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
......
......@@ -37,6 +37,8 @@ from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import xqueue_interface
from codejail.safe_exec import safe_exec
log = logging.getLogger(__name__)
......@@ -968,14 +970,20 @@ def sympy_check2():
cfn = xml.get('cfn')
if cfn:
log.debug("cfn = %s" % cfn)
if cfn in self.context:
self.code = self.context[cfn]
else:
msg = "%s: can't find cfn %s in context" % (
unicode(self), cfn)
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline',
'<unavailable>')
raise LoncapaProblemError(msg)
def make_check_function(script_code, cfn):
def check_function(expect, ans):
code = (script_code + "\n" +
"cfn_return = %s(expect, ans)\n" % cfn)
globals_dict = {
'expect': expect,
'ans': ans,
}
safe_exec(code, globals_dict)
return globals_dict['cfn_return']
return check_function
self.code = make_check_function(self.context['script_code'], cfn)
if not self.code:
if answer is None:
......@@ -1074,6 +1082,7 @@ def sympy_check2():
# exec the check function
if isinstance(self.code, basestring):
try:
raise Exception("exec 1")
exec self.code in self.context['global_context'], self.context
correct = self.context['correct']
messages = self.context['messages']
......@@ -1083,32 +1092,15 @@ def sympy_check2():
self._handle_exec_exception(err)
else:
# self.code is not a string; assume its a function
# self.code is not a string; it's a function we created earlier.
# this is an interface to the Tutor2 check functions
fn = self.code
ret = None
log.debug(" submission = %s" % submission)
try:
answer_given = submission[0] if (
len(idset) == 1) else submission
# handle variable number of arguments in check function, for backwards compatibility
# with various Tutor2 check functions
args = [self.expect, answer_given,
student_answers, self.answer_ids[0]]
argspec = inspect.getargspec(fn)
nargs = len(argspec.args) - len(argspec.defaults or [])
kwargs = {}
for argname in argspec.args[nargs:]:
kwargs[argname] = self.context[
argname] if argname in self.context else None
log.debug('[customresponse] answer_given=%s' % answer_given)
log.debug('nargs=%d, args=%s, kwargs=%s' % (
nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs)
answer_given = submission[0] if (len(idset) == 1) else submission
ret = fn(self.expect, answer_given)
except Exception as err:
self._handle_exec_exception(err)
......@@ -1265,6 +1257,7 @@ class SymbolicResponse(CustomResponse):
def setup_response(self):
self.xml.set('cfn', 'symmath_check')
code = "from symmath import *"
raise Exception("exec 2")
exec code in self.context, self.context
CustomResponse.setup_response(self)
......@@ -1378,6 +1371,7 @@ class CodeResponse(LoncapaResponse):
penv = {}
penv['__builtins__'] = globals()['__builtins__']
try:
raise Exception("exec 3")
exec(code, penv, penv)
except Exception as err:
log.error(
......@@ -1925,18 +1919,12 @@ class SchematicResponse(LoncapaResponse):
self.code = answer.text
def get_score(self, student_answers):
from capa_problem import global_context
submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)]
#from capa_problem import global_context
submission = [
json.loads(student_answers[k]) for k in sorted(self.answer_ids)
]
self.context.update({'submission': submission})
try:
exec self.code in global_context, self.context
except Exception as err:
_, _, traceback_obj = sys.exc_info()
raise ResponseError, ResponseError(err.message), traceback_obj
safe_exec(self.code, {}, self.context)
cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct'])))
......
......@@ -19,6 +19,11 @@ def jsonable_dict(d):
return jd
def safe_exec(code, globals_dict, locals_dict=None, future_division=False, assumed_imports=None):
"""Execute code safely.
Returns None. The code can modify globals in `global_dict`.
"""
if future_division:
code = "from __future__ import division\n" + code
......
<course>
<chapter url_name="GradedChapter">
<chapter url_name="EmbeddedPythonChapter">
<vertical url_name="Homework1">
<problem url_name="H1P1">
<problem url_name="schematic_problem">
<schematicresponse>
<center>
<schematic height="500" width="600" parts="g,n,s" analyses="dc,tran" submit_analyses="{&quot;tran&quot;:[[&quot;Z&quot;,0.0000004,0.0000009,0.0000014,0.0000019,0.0000024,0.0000029,0.0000034,0.000039]]}" initial_value="[[&quot;w&quot;,[112,96,128,96]],[&quot;w&quot;,[256,96,240,96]],[&quot;w&quot;,[192,96,240,96]],[&quot;s&quot;,[240,96,0],{&quot;color&quot;:&quot;cyan&quot;,&quot;offset&quot;:&quot;&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:3},[&quot;Z&quot;]],[&quot;w&quot;,[32,224,192,224]],[&quot;w&quot;,[96,48,192,48]],[&quot;L&quot;,[256,96,3],{&quot;label&quot;:&quot;Z&quot;,&quot;_json_&quot;:6},[&quot;Z&quot;]],[&quot;r&quot;,[192,48,0],{&quot;name&quot;:&quot;Rpullup&quot;,&quot;r&quot;:&quot;10K&quot;,&quot;_json_&quot;:7},[&quot;1&quot;,&quot;Z&quot;]],[&quot;w&quot;,[32,144,32,192]],[&quot;w&quot;,[32,224,32,192]],[&quot;w&quot;,[48,192,32,192]],[&quot;w&quot;,[32,96,32,144]],[&quot;w&quot;,[48,144,32,144]],[&quot;w&quot;,[32,48,32,96]],[&quot;w&quot;,[48,96,32,96]],[&quot;w&quot;,[32,48,48,48]],[&quot;g&quot;,[32,224,0],{&quot;_json_&quot;:16},[&quot;0&quot;]],[&quot;v&quot;,[96,192,1],{&quot;name&quot;:&quot;VC&quot;,&quot;value&quot;:&quot;square(3,0,250K)&quot;,&quot;_json_&quot;:17},[&quot;C&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,144,1],{&quot;name&quot;:&quot;VB&quot;,&quot;value&quot;:&quot;square(3,0,500K)&quot;,&quot;_json_&quot;:18},[&quot;B&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,96,1],{&quot;name&quot;:&quot;VA&quot;,&quot;value&quot;:&quot;square(3,0,1000K)&quot;,&quot;_json_&quot;:19},[&quot;A&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,48,1],{&quot;name&quot;:&quot;Vpwr&quot;,&quot;value&quot;:&quot;dc(3)&quot;,&quot;_json_&quot;:20},[&quot;1&quot;,&quot;0&quot;]],[&quot;L&quot;,[96,96,2],{&quot;label&quot;:&quot;A&quot;,&quot;_json_&quot;:21},[&quot;A&quot;]],[&quot;w&quot;,[96,96,104,96]],[&quot;L&quot;,[96,144,2],{&quot;label&quot;:&quot;B&quot;,&quot;_json_&quot;:23},[&quot;B&quot;]],[&quot;w&quot;,[96,144,104,144]],[&quot;L&quot;,[96,192,2],{&quot;label&quot;:&quot;C&quot;,&quot;_json_&quot;:25},[&quot;C&quot;]],[&quot;w&quot;,[96,192,104,192]],[&quot;w&quot;,[192,96,192,112]],[&quot;s&quot;,[112,96,0],{&quot;color&quot;:&quot;red&quot;,&quot;offset&quot;:&quot;15&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:28},[&quot;A&quot;]],[&quot;w&quot;,[104,96,112,96]],[&quot;s&quot;,[112,144,0],{&quot;color&quot;:&quot;green&quot;,&quot;offset&quot;:&quot;10&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:30},[&quot;B&quot;]],[&quot;w&quot;,[104,144,112,144]],[&quot;w&quot;,[128,144,112,144]],[&quot;s&quot;,[112,192,0],{&quot;color&quot;:&quot;blue&quot;,&quot;offset&quot;:&quot;5&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:33},[&quot;C&quot;]],[&quot;w&quot;,[104,192,112,192]],[&quot;w&quot;,[128,192,112,192]],[&quot;view&quot;,0,0,2,&quot;5&quot;,&quot;10&quot;,&quot;10MEG&quot;,null,&quot;100&quot;,&quot;4us&quot;]]"/>
<schematic height="500" width="600" parts="g,n,s" analyses="dc,tran"
submit_analyses="{&quot;tran&quot;:[[&quot;Z&quot;,0.0000004,0.0000009,0.0000014,0.0000019,0.0000024,0.0000029,0.0000034,0.000039]]}"
initial_value="[[&quot;w&quot;,[112,96,128,96]],[&quot;w&quot;,[256,96,240,96]],[&quot;w&quot;,[192,96,240,96]],[&quot;s&quot;,[240,96,0],{&quot;color&quot;:&quot;cyan&quot;,&quot;offset&quot;:&quot;&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:3},[&quot;Z&quot;]],[&quot;w&quot;,[32,224,192,224]],[&quot;w&quot;,[96,48,192,48]],[&quot;L&quot;,[256,96,3],{&quot;label&quot;:&quot;Z&quot;,&quot;_json_&quot;:6},[&quot;Z&quot;]],[&quot;r&quot;,[192,48,0],{&quot;name&quot;:&quot;Rpullup&quot;,&quot;r&quot;:&quot;10K&quot;,&quot;_json_&quot;:7},[&quot;1&quot;,&quot;Z&quot;]],[&quot;w&quot;,[32,144,32,192]],[&quot;w&quot;,[32,224,32,192]],[&quot;w&quot;,[48,192,32,192]],[&quot;w&quot;,[32,96,32,144]],[&quot;w&quot;,[48,144,32,144]],[&quot;w&quot;,[32,48,32,96]],[&quot;w&quot;,[48,96,32,96]],[&quot;w&quot;,[32,48,48,48]],[&quot;g&quot;,[32,224,0],{&quot;_json_&quot;:16},[&quot;0&quot;]],[&quot;v&quot;,[96,192,1],{&quot;name&quot;:&quot;VC&quot;,&quot;value&quot;:&quot;square(3,0,250K)&quot;,&quot;_json_&quot;:17},[&quot;C&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,144,1],{&quot;name&quot;:&quot;VB&quot;,&quot;value&quot;:&quot;square(3,0,500K)&quot;,&quot;_json_&quot;:18},[&quot;B&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,96,1],{&quot;name&quot;:&quot;VA&quot;,&quot;value&quot;:&quot;square(3,0,1000K)&quot;,&quot;_json_&quot;:19},[&quot;A&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,48,1],{&quot;name&quot;:&quot;Vpwr&quot;,&quot;value&quot;:&quot;dc(3)&quot;,&quot;_json_&quot;:20},[&quot;1&quot;,&quot;0&quot;]],[&quot;L&quot;,[96,96,2],{&quot;label&quot;:&quot;A&quot;,&quot;_json_&quot;:21},[&quot;A&quot;]],[&quot;w&quot;,[96,96,104,96]],[&quot;L&quot;,[96,144,2],{&quot;label&quot;:&quot;B&quot;,&quot;_json_&quot;:23},[&quot;B&quot;]],[&quot;w&quot;,[96,144,104,144]],[&quot;L&quot;,[96,192,2],{&quot;label&quot;:&quot;C&quot;,&quot;_json_&quot;:25},[&quot;C&quot;]],[&quot;w&quot;,[96,192,104,192]],[&quot;w&quot;,[192,96,192,112]],[&quot;s&quot;,[112,96,0],{&quot;color&quot;:&quot;red&quot;,&quot;offset&quot;:&quot;15&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:28},[&quot;A&quot;]],[&quot;w&quot;,[104,96,112,96]],[&quot;s&quot;,[112,144,0],{&quot;color&quot;:&quot;green&quot;,&quot;offset&quot;:&quot;10&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:30},[&quot;B&quot;]],[&quot;w&quot;,[104,144,112,144]],[&quot;w&quot;,[128,144,112,144]],[&quot;s&quot;,[112,192,0],{&quot;color&quot;:&quot;blue&quot;,&quot;offset&quot;:&quot;5&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:33},[&quot;C&quot;]],[&quot;w&quot;,[104,192,112,192]],[&quot;w&quot;,[128,192,112,192]],[&quot;view&quot;,0,0,2,&quot;5&quot;,&quot;10&quot;,&quot;10MEG&quot;,null,&quot;100&quot;,&quot;4us&quot;]]"
/>
</center>
<answer type="loncapa/python">
# for a schematic response, submission[i] is the json representation
......@@ -44,6 +47,51 @@ correct = ['correct' if okay else 'incorrect']
</problem>
<problem url_name="cfn_problem">
<text>
<script type="text/python" system_path="python_lib">
def test_csv(expect, ans):
# Take out all spaces in expected answer
expect = [i.strip(' ') for i in str(expect).split(',')]
# Take out all spaces in student solution
ans = [i.strip(' ') for i in str(ans).split(',')]
def strip_q(x):
# Strip quotes around strings if students have entered them
stripped_ans = []
for item in x:
if item[0] == "'" and item[-1]=="'":
item = item.strip("'")
elif item[0] == '"' and item[-1] == '"':
item = item.strip('"')
stripped_ans.append(item)
return stripped_ans
return strip_q(expect) == strip_q(ans)
</script>
<ol class="enumerate">
<li>
<pre>
num = 0
while num &lt;= 5:
print(num)
num += 1
print("Outside of loop")
print(num)
</pre>
<p>
<customresponse cfn="test_csv" expect="0, 1, 2, 3, 4, 5, 'Outside of loop', 6">
<textline size="50" correct_answer="0, 1, 2, 3, 4, 5, 'Outside of loop', 6"/>
</customresponse>
</p>
</li>
</ol>
</text>
</problem>
</vertical>
</chapter>
</course>
......@@ -986,7 +986,7 @@ class TestSchematicResponse(TestSubmittingProblems):
return resp
def test_get_graded(self):
resp = self.submit_question_answer('H1P1',
resp = self.submit_question_answer('schematic_problem',
[['transient', {'Z': [
[0.0000004, 2.8],
[0.0000009, 2.8],
......@@ -1001,8 +1001,8 @@ class TestSchematicResponse(TestSubmittingProblems):
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'correct')
self.reset_question_answer('H1P1')
resp = self.submit_question_answer('H1P1',
self.reset_question_answer('schematic_problem')
resp = self.submit_question_answer('schematic_problem',
[['transient', {'Z': [
[0.0000004, 2.8],
[0.0000009, 0.0], # wrong.
......@@ -1016,3 +1016,31 @@ class TestSchematicResponse(TestSubmittingProblems):
)
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'incorrect')
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCustomResponseCfnFunction(TestSubmittingProblems):
"""Check that cfn functions work properly."""
course_slug = "embedded_python"
course_when = "2013_Spring"
def submit_question_answer(self, problem_url_name, responses):
"""Particular to the embedded_python/2013_Spring course."""
problem_location = self.problem_location(problem_url_name)
modx_url = self.modx_url(problem_location, 'problem_check')
resp = self.client.post(modx_url, {
'input_i4x-edX-embedded_python-problem-{0}_2_1'.format(problem_url_name): responses,
})
return resp
def test_get_graded(self):
resp = self.submit_question_answer('cfn_problem', "0, 1, 2, 3, 4, 5, 'Outside of loop', 6")
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'correct')
self.reset_question_answer('cfn_problem')
resp = self.submit_question_answer('cfn_problem', "xyzzy!")
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'incorrect')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment