diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 6580114bcc..6973deb4a0 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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 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('<', '<') diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b06a62ffc6..f732c9fc84 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -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', - '') - 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']))) diff --git a/common/lib/codejail/codejail/safe_exec.py b/common/lib/codejail/codejail/safe_exec.py index eeb62d5bbd..92beba98cc 100644 --- a/common/lib/codejail/codejail/safe_exec.py +++ b/common/lib/codejail/codejail/safe_exec.py @@ -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 diff --git a/common/test/data/embedded_python/course/2013_Spring.xml b/common/test/data/embedded_python/course/2013_Spring.xml index cfa67ff0f3..15e11befd1 100644 --- a/common/test/data/embedded_python/course/2013_Spring.xml +++ b/common/test/data/embedded_python/course/2013_Spring.xml @@ -1,11 +1,14 @@ - + - +
- +
# for a schematic response, submission[i] is the json representation @@ -44,6 +47,51 @@ correct = ['correct' if okay else 'incorrect']
+ + + + + +
    +
  1. +
    +num = 0
    +while num <= 5:
    +    print(num)
    +    num += 1
    +
    +print("Outside of loop")
    +print(num)
    + 
    +

    + + + +

    +
  2. +
+
+
+
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 2f469fe750..acad3e9c84 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -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')