Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'MOODLE_24_STABLE' into install_24_STABLE

  • Loading branch information...
commit e417377e897e31f7308b40e73e74b9ded0714a84 2 parents dadf5df + c110815
AMOS bot authored
Showing with 465 additions and 381 deletions.
  1. +1 −15 auth/shibboleth/index.php
  2. +1 −0  grade/grading/form/guide/guideeditor.php
  3. +1 −1  grade/grading/form/guide/js/guide.js
  4. +2 −2 grade/grading/form/guide/js/guideeditor.js
  5. +1 −1  grade/grading/form/guide/lib.php
  6. +15 −15 grade/grading/form/guide/renderer.php
  7. +2 −2 grade/grading/form/rubric/js/rubriceditor.js
  8. +6 −6 grade/grading/form/rubric/renderer.php
  9. +1 −0  grade/grading/form/rubric/rubriceditor.php
  10. +21 −1 lib/form/editor.php
  11. +20 −2 lib/form/filemanager.php
  12. +1 −37 lib/moodlelib.php
  13. +11 −10 lib/yui/notification/notification.js
  14. +2 −0  mod/imscp/locallib.php
  15. +8 −1 mod/lti/service.php
  16. +7 −5 notes/index.php
  17. +7 −0 question/format.php
  18. +2 −5 question/format/webct/format.php
  19. +2 −2 question/format/xml/format.php
  20. +24 −21 question/type/calculated/edit_calculated_form.php
  21. +8 −100 question/type/calculated/question.php
  22. +103 −65 question/type/calculated/questiontype.php
  23. +105 −0 question/type/calculated/tests/formula_validation_test.php
  24. +26 −40 question/type/calculatedmulti/edit_calculatedmulti_form.php
  25. +20 −6 question/type/calculatedmulti/questiontype.php
  26. +4 −9 question/type/calculatedsimple/questiontype.php
  27. +5 −5 repository/coursefiles/lib.php
  28. +1 −1  repository/equella/callback.php
  29. +10 −5 repository/equella/lib.php
  30. +1 −1  repository/filepicker.php
  31. +36 −13 repository/lib.php
  32. +3 −3 repository/local/lib.php
  33. +3 −2 repository/recent/lib.php
  34. +1 −1  repository/repository_ajax.php
  35. +4 −4 repository/user/lib.php
View
16 auth/shibboleth/index.php
@@ -47,21 +47,7 @@
if ($shibbolethauth->user_login($frm->username, $frm->password)
&& $user = authenticate_user_login($frm->username, $frm->password)) {
- enrol_check_plugins($user);
- session_set_user($user);
-
- $USER->loggedin = true;
- $USER->site = $CFG->wwwroot; // for added security, store the site in the
-
- update_user_login_times();
-
- // Don't show previous shibboleth username on login page
-
- set_login_session_preferences();
-
- unset($SESSION->lang);
- $SESSION->justloggedin = true;
-
+ complete_user_login($user);
add_to_log(SITEID, 'user', 'login', "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
if (user_not_fully_set_up($USER)) {
View
1  grade/grading/form/guide/guideeditor.php
@@ -100,6 +100,7 @@ public function toHtml() {
$mode = gradingform_guide_controller::DISPLAY_EDIT_FULL;
$module = array('name'=>'gradingform_guideeditor',
'fullpath'=>'/grade/grading/form/guide/js/guideeditor.js',
+ 'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
'strings' => array(
array('confirmdeletecriterion', 'gradingform_guide'),
array('clicktoedit', 'gradingform_guide'),
View
2  grade/grading/form/guide/js/guide.js
@@ -10,7 +10,7 @@ M.gradingform_guide.init = function(Y, options) {
currentfocus = e.currentTarget;
});
Y.all('.markingguidecomment').on('click', function(e) {
- currentfocus.set('value', currentfocus.get('value') + '\n' + e.currentTarget.get('innerHTML'));
+ currentfocus.set('value', currentfocus.get('value') + '\n' + e.currentTarget.get('text'));
currentfocus.focus();
});
View
4 grade/grading/form/guide/js/guideeditor.js
@@ -111,9 +111,9 @@ M.gradingform_guideeditor.editmode = function(el, editmode) {
value = M.str.gradingform_guide.clicktoedit
taplain.addClass('empty')
}
- taplain.one('.textvalue').set('innerHTML', value)
+ taplain.one('.textvalue').set('innerHTML', Y.Escape.html(value))
if (tb) {
- tbplain.one('.textvalue').set('innerHTML', tb.get('value'))
+ tbplain.one('.textvalue').set('innerHTML', Y.Escape.html(tb.get('value')))
}
// hide/display textarea, textbox and plaintexts
taplain.removeClass('hiddenelement')
View
2  grade/grading/form/guide/lib.php
@@ -845,7 +845,7 @@ public function render_grading_element($page, $gradingformelement) {
if (!empty($this->validationerrors)) {
foreach ($this->validationerrors as $id => $err) {
$a = new stdClass();
- $a->criterianame = $criteria[$id]['shortname'];
+ $a->criterianame = s($criteria[$id]['shortname']);
$a->maxscore = $criteria[$id]['maxscore'];
$html .= html_writer::tag('div', get_string('err_scoreinvalid', 'gradingform_guide', $a),
array('class' => 'gradingform_guide-error'));
View
30 grade/grading/form/guide/renderer.php
@@ -93,20 +93,20 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
$shortname = html_writer::empty_tag('input', array('type'=> 'text',
- 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'value' => htmlspecialchars($criterion['shortname']),
+ 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'value' => $criterion['shortname'],
'id ' => '{NAME}[criteria][{CRITERION-id}][shortname]'));
$shortname = html_writer::tag('div', $shortname, array('class'=>'criterionname'));
- $description = html_writer::tag('textarea', htmlspecialchars($criterion['description']),
+ $description = html_writer::tag('textarea', s($criterion['description']),
array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5'));
$description = html_writer::tag('div', $description, array('class'=>'criteriondesc'));
- $descriptionmarkers = html_writer::tag('textarea', htmlspecialchars($criterion['descriptionmarkers']),
+ $descriptionmarkers = html_writer::tag('textarea', s($criterion['descriptionmarkers']),
array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5'));
$descriptionmarkers = html_writer::tag('div', $descriptionmarkers, array('class'=>'criteriondescmarkers'));
$maxscore = html_writer::empty_tag('input', array('type'=> 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'size' => '3',
- 'value' => htmlspecialchars($criterion['maxscore']),
+ 'value' => $criterion['maxscore'],
'id' => '{NAME}[criteria][{CRITERION-id}][maxscore]'));
$maxscore = html_writer::tag('div', $maxscore, array('class'=>'criterionmaxscore'));
} else {
@@ -125,7 +125,7 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
$descriptionclass = 'descriptionreadonly';
}
- $shortname = html_writer::tag('div', $criterion['shortname'],
+ $shortname = html_writer::tag('div', s($criterion['shortname']),
array('class'=>'criterionshortname', 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]'));
$descmarkerclass = '';
$descstudentclass = '';
@@ -137,13 +137,13 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
$descstudentclass = ' hide';
}
}
- $description = html_writer::tag('div', $criterion['description'],
+ $description = html_writer::tag('div', s($criterion['description']),
array('class'=>'criteriondescription'.$descstudentclass,
'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]'));
- $descriptionmarkers = html_writer::tag('div', $criterion['descriptionmarkers'],
+ $descriptionmarkers = html_writer::tag('div', s($criterion['descriptionmarkers']),
array('class'=>'criteriondescriptionmarkers'.$descmarkerclass,
'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]'));
- $maxscore = html_writer::tag('div', $criterion['maxscore'],
+ $maxscore = html_writer::tag('div', s($criterion['maxscore']),
array('class'=>'criteriondescriptionscore', 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]'));
}
@@ -188,7 +188,7 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
$scoreclass = 'error';
$currentscore = $validationerrors[$criterion['id']]['score']; // Show invalid score in form.
}
- $input = html_writer::tag('textarea', htmlspecialchars($currentremark),
+ $input = html_writer::tag('textarea', s($currentremark),
array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '65', 'rows' => '5',
'class' => 'markingguideremark'));
$criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
@@ -197,7 +197,7 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
$score .= html_writer::empty_tag('input', array('type'=> 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass,
'id' => '{NAME}[criteria][{CRITERION-id}][score]',
- 'size' => '3', 'value' => htmlspecialchars($currentscore)));
+ 'size' => '3', 'value' => $currentscore));
$score .= '/'.$maxscore;
$criteriontemplate .= html_writer::tag('td', $score, array('class' => 'score'));
@@ -206,9 +206,9 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
} else if ($mode == gradingform_guide_controller::DISPLAY_REVIEW ||
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
- $criteriontemplate .= html_writer::tag('td', $currentremark, array('class' => 'remark'));
+ $criteriontemplate .= html_writer::tag('td', s($currentremark), array('class' => 'remark'));
if (!empty($options['showmarkspercriterionstudents'])) {
- $criteriontemplate .= html_writer::tag('td', htmlspecialchars($currentscore). ' / '.$maxscore,
+ $criteriontemplate .= html_writer::tag('td', s($currentscore). ' / '.$maxscore,
array('class' => 'score'));
}
}
@@ -267,7 +267,7 @@ public function comment_template($mode, $elementname = '{NAME}', $comment = null
$criteriontemplate .= html_writer::end_tag('td'); // Controls.
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder']));
- $description = html_writer::tag('textarea', htmlspecialchars($comment['description']),
+ $description = html_writer::tag('textarea', s($comment['description']),
array('name' => '{NAME}[comments][{COMMENT-id}][description]', 'cols' => '65', 'rows' => '5'));
$description = html_writer::tag('div', $description, array('class'=>'criteriondesc'));
} else {
@@ -278,12 +278,12 @@ public function comment_template($mode, $elementname = '{NAME}', $comment = null
'name' => '{NAME}[comments][{COMMENT-id}][description]', 'value' => $comment['description']));
}
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
- $description = html_writer::tag('span', htmlspecialchars($comment['description']),
+ $description = html_writer::tag('span', s($comment['description']),
array('name' => '{NAME}[comments][{COMMENT-id}][description]',
'title' => get_string('clicktocopy', 'gradingform_guide'),
'id' => '{NAME}[comments][{COMMENT-id}]', 'class'=>'markingguidecomment'));
} else {
- $description = $comment['description'];
+ $description = s($comment['description']);
}
}
$descriptionclass = 'description';
View
4 grade/grading/form/rubric/js/rubriceditor.js
@@ -93,8 +93,8 @@ M.gradingform_rubriceditor.editmode = function(el, editmode, focustb) {
value = (el.hasClass('level')) ? M.str.gradingform_rubric.levelempty : M.str.gradingform_rubric.criterionempty
taplain.addClass('empty')
}
- taplain.one('.textvalue').set('innerHTML', value)
- if (tb) tbplain.one('.textvalue').set('innerHTML', tb.get('value'))
+ taplain.one('.textvalue').set('innerHTML', Y.Escape.html(value));
+ if (tb) tbplain.one('.textvalue').set('innerHTML', Y.Escape.html(tb.get('value')));
// hide/display textarea, textbox and plaintexts
taplain.removeClass('hiddenelement')
ta.addClass('hiddenelement')
View
12 grade/grading/form/rubric/renderer.php
@@ -74,13 +74,13 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
}
$criteriontemplate .= html_writer::end_tag('td'); // .controls
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
- $description = html_writer::tag('textarea', htmlspecialchars($criterion['description']), array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '10', 'rows' => '5'));
+ $description = html_writer::tag('textarea', s($criterion['description']), array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '10', 'rows' => '5'));
} else {
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description']));
}
- $description = $criterion['description'];
+ $description = s($criterion['description']);
}
$descriptionclass = 'description';
if (isset($criterion['error_description'])) {
@@ -106,12 +106,12 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr
$currentremark = $value['remark'];
}
if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
- $input = html_writer::tag('textarea', htmlspecialchars($currentremark), array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '10', 'rows' => '5'));
+ $input = html_writer::tag('textarea', s($currentremark), array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '10', 'rows' => '5'));
$criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
} else if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
}else if ($mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW) {
- $criteriontemplate .= html_writer::tag('td', $currentremark, array('class' => 'remark')); // TODO maybe some prefix here like 'Teacher remark:'
+ $criteriontemplate .= html_writer::tag('td', s($currentremark), array('class' => 'remark'));
}
}
$criteriontemplate .= html_writer::end_tag('tr'); // .criterion
@@ -163,7 +163,7 @@ public function level_template($mode, $options, $elementname = '{NAME}', $criter
$leveltemplate = html_writer::start_tag('td', $tdattributes);
$leveltemplate .= html_writer::start_tag('div', array('class' => 'level-wrapper'));
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
- $definition = html_writer::tag('textarea', htmlspecialchars($level['definition']), array('name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4'));
+ $definition = html_writer::tag('textarea', s($level['definition']), array('name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4'));
$score = html_writer::label(get_string('criterionempty', 'gradingform_rubric'), '{NAME}criteria{CRITERION-id}levels{LEVEL-id}', false, array('class' => 'accesshide'));
$score .= html_writer::empty_tag('input', array('type' => 'text','id' => '{NAME}criteria{CRITERION-id}levels{LEVEL-id}', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '3', 'value' => $level['score']));
} else {
@@ -171,7 +171,7 @@ public function level_template($mode, $options, $elementname = '{NAME}', $criter
$leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition']));
$leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'value' => $level['score']));
}
- $definition = $level['definition'];
+ $definition = s($level['definition']);
$score = $level['score'];
}
if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
View
1  grade/grading/form/rubric/rubriceditor.php
@@ -85,6 +85,7 @@ public function toHtml() {
if (!$this->_flagFrozen) {
$mode = gradingform_rubric_controller::DISPLAY_EDIT_FULL;
$module = array('name'=>'gradingform_rubriceditor', 'fullpath'=>'/grade/grading/form/rubric/js/rubriceditor.js',
+ 'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'),
array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric')
));
View
22 lib/form/editor.php
@@ -95,6 +95,25 @@ function MoodleQuickForm_editor($elementName=null, $elementLabel=null, $attribut
}
/**
+ * Called by HTML_QuickForm whenever form event is made on this element
+ *
+ * @param string $event Name of event
+ * @param mixed $arg event arguments
+ * @param object $caller calling object
+ * @return bool
+ */
+ function onQuickFormEvent($event, $arg, &$caller)
+ {
+ switch ($event) {
+ case 'createElement':
+ $caller->setType($arg[0] . '[format]', PARAM_ALPHANUM);
+ $caller->setType($arg[0] . '[itemid]', PARAM_INT);
+ break;
+ }
+ return parent::onQuickFormEvent($event, $arg, $caller);
+ }
+
+ /**
* Sets name of editor
*
* @param string $name name of the editor
@@ -403,7 +422,8 @@ function toHtml() {
if (!during_initial_install() && empty($CFG->adminsetuppending)) {
// 0 means no files, -1 unlimited
if ($maxfiles != 0 ) {
- $str .= '<input type="hidden" name="'.$elname.'[itemid]" value="'.$draftitemid.'" />';
+ $str .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $elname.'[itemid]',
+ 'value' => $draftitemid));
// used by non js editor only
$editorurl = new moodle_url("$CFG->wwwroot/repository/draftfiles_manager.php", array(
View
22 lib/form/filemanager.php
@@ -80,6 +80,24 @@ function MoodleQuickForm_filemanager($elementName=null, $elementLabel=null, $att
}
/**
+ * Called by HTML_QuickForm whenever form event is made on this element
+ *
+ * @param string $event Name of event
+ * @param mixed $arg event arguments
+ * @param object $caller calling object
+ * @return bool
+ */
+ function onQuickFormEvent($event, $arg, &$caller)
+ {
+ switch ($event) {
+ case 'createElement':
+ $caller->setType($arg[0], PARAM_INT);
+ break;
+ }
+ return parent::onQuickFormEvent($event, $arg, $caller);
+ }
+
+ /**
* Sets name of filemanager
*
* @param string $name name of the filemanager
@@ -263,9 +281,9 @@ function toHtml() {
$output = $PAGE->get_renderer('core', 'files');
$html .= $output->render($fm);
- $html .= '<input value="'.$draftitemid.'" name="'.$elname.'" type="hidden" />';
+ $html .= html_writer::empty_tag('input', array('value' => $draftitemid, 'name' => $elname, 'type' => 'hidden'));
// label element needs 'for' attribute work
- $html .= '<input value="" id="id_'.$elname.'" type="hidden" />';
+ $html .= html_writer::empty_tag('input', array('value' => '', 'id' => 'id_'.$elname, 'type' => 'hidden'));
return $html;
}
View
38 lib/moodlelib.php
@@ -10314,43 +10314,7 @@ function message_popup_window() {
//if we have new messages to notify the user about
if (!empty($message_users)) {
- $strmessages = '';
- if (count($message_users)>1) {
- $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
- } else {
- $message_users = reset($message_users);
-
- //show who the message is from if its not a notification
- if (!$message_users->notification) {
- $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
- }
-
- //try to display the small version of the message
- $smallmessage = null;
- if (!empty($message_users->smallmessage)) {
- //display the first 200 chars of the message in the popup
- $smallmessage = null;
- if (textlib::strlen($message_users->smallmessage) > 200) {
- $smallmessage = textlib::substr($message_users->smallmessage,0,200).'...';
- } else {
- $smallmessage = $message_users->smallmessage;
- }
-
- //prevent html symbols being displayed
- if ($message_users->fullmessageformat == FORMAT_HTML) {
- $smallmessage = html_to_text($smallmessage);
- } else {
- $smallmessage = s($smallmessage);
- }
- } else if ($message_users->notification) {
- //its a notification with no smallmessage so just say they have a notification
- $smallmessage = get_string('unreadnewnotification', 'message');
- }
- if (!empty($smallmessage)) {
- $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
- }
- }
-
+ $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
$strgomessage = get_string('gotomessages', 'message');
$strstaymessage = get_string('ignore','admin');
View
21 lib/yui/notification/notification.js
@@ -254,11 +254,11 @@ Y.extend(EXCEPTION, DIALOGUE, {
_keypress : null,
initializer : function(config) {
this.get(BASE).addClass('moodle-dialogue-exception');
- this.setStdModContent(Y.WidgetStdMod.HEADER, '<h1 id="moodle-dialogue-'+COUNT+'-header-text">' + config.name + '</h1>', Y.WidgetStdMod.REPLACE);
+ this.setStdModContent(Y.WidgetStdMod.HEADER, '<h1 id="moodle-dialogue-'+COUNT+'-header-text">' + Y.Escape.html(config.name) + '</h1>', Y.WidgetStdMod.REPLACE);
var content = C('<div class="moodle-exception"></div>')
- .append(C('<div class="moodle-exception-message">'+this.get('message')+'</div>'))
- .append(C('<div class="moodle-exception-param hidden param-filename"><label>File:</label> '+this.get('fileName')+'</div>'))
- .append(C('<div class="moodle-exception-param hidden param-linenumber"><label>Line:</label> '+this.get('lineNumber')+'</div>'))
+ .append(C('<div class="moodle-exception-message">'+Y.Escape.html(this.get('message'))+'</div>'))
+ .append(C('<div class="moodle-exception-param hidden param-filename"><label>File:</label> '+Y.Escape.html(this.get('fileName'))+'</div>'))
+ .append(C('<div class="moodle-exception-param hidden param-linenumber"><label>Line:</label> '+Y.Escape.html(this.get('lineNumber'))+'</div>'))
.append(C('<div class="moodle-exception-param hidden param-stacktrace"><label>Stack trace:</label> <pre>'+this.get('stack')+'</pre></div>'));
if (M.cfg.developerdebug) {
content.all('.moodle-exception-param').removeClass('hidden');
@@ -300,7 +300,7 @@ Y.extend(EXCEPTION, DIALOGUE, {
},
stack : {
setter : function(str) {
- var lines = str.split("\n");
+ var lines = Y.Escape.html(str).split("\n");
var pattern = new RegExp('^(.+)@('+M.cfg.wwwroot+')?(.{0,75}).*:(\\d+)$');
for (var i in lines) {
lines[i] = lines[i].replace(pattern, "<div class='stacktrace-line'>ln: $4</div><div class='stacktrace-file'>$3</div><div class='stacktrace-call'>$1</div>");
@@ -325,12 +325,12 @@ Y.extend(AJAXEXCEPTION, DIALOGUE, {
_keypress : null,
initializer : function(config) {
this.get(BASE).addClass('moodle-dialogue-exception');
- this.setStdModContent(Y.WidgetStdMod.HEADER, '<h1 id="moodle-dialogue-'+COUNT+'-header-text">' + config.name + '</h1>', Y.WidgetStdMod.REPLACE);
+ this.setStdModContent(Y.WidgetStdMod.HEADER, '<h1 id="moodle-dialogue-'+COUNT+'-header-text">' + Y.Escape.html(config.name) + '</h1>', Y.WidgetStdMod.REPLACE);
var content = C('<div class="moodle-ajaxexception"></div>')
- .append(C('<div class="moodle-exception-message">'+this.get('error')+'</div>'))
+ .append(C('<div class="moodle-exception-message">'+Y.Escape.html(this.get('error'))+'</div>'))
.append(C('<div class="moodle-exception-param hidden param-debuginfo"><label>URL:</label> '+this.get('reproductionlink')+'</div>'))
- .append(C('<div class="moodle-exception-param hidden param-debuginfo"><label>Debug info:</label> '+this.get('debuginfo')+'</div>'))
- .append(C('<div class="moodle-exception-param hidden param-stacktrace"><label>Stack trace:</label> <pre>'+this.get('stacktrace')+'</pre></div>'));
+ .append(C('<div class="moodle-exception-param hidden param-debuginfo"><label>Debug info:</label> '+Y.Escape.html(this.get('debuginfo'))+'</div>'))
+ .append(C('<div class="moodle-exception-param hidden param-stacktrace"><label>Stack trace:</label> <pre>'+Y.Escape.html(this.get('stacktrace'))+'</pre></div>'));
if (M.cfg.developerdebug) {
content.all('.moodle-exception-param').removeClass('hidden');
}
@@ -369,6 +369,7 @@ Y.extend(AJAXEXCEPTION, DIALOGUE, {
reproductionlink : {
setter : function(link) {
if (link !== null) {
+ link = Y.Escape.html(link)
link = '<a href="'+link+'">'+link.replace(M.cfg.wwwroot, '')+'</a>';
}
return link;
@@ -389,4 +390,4 @@ M.core.confirm = CONFIRM;
M.core.exception = EXCEPTION;
M.core.ajaxException = AJAXEXCEPTION;
-}, '@VERSION@', {requires:['base','node','panel','event-key', 'moodle-core-notification-skin', 'dd-plugin']});
+}, '@VERSION@', {requires:['base','node','panel','escape','event-key', 'moodle-core-notification-skin', 'dd-plugin']});
View
2  mod/imscp/locallib.php
@@ -105,9 +105,11 @@ function imscp_parse_structure($imscp, $context) {
*/
function imscp_parse_manifestfile($manifestfilecontents) {
$doc = new DOMDocument();
+ $oldentities = libxml_disable_entity_loader(true);
if (!$doc->loadXML($manifestfilecontents, LIBXML_NONET)) {
return null;
}
+ libxml_disable_entity_loader($oldentities);
// we put this fake URL as base in order to detect path changes caused by xml:base attributes
$doc->documentURI = 'http://grrr/';
View
9 mod/lti/service.php
@@ -53,7 +53,14 @@
throw new Exception('Message signature not valid');
}
-$xml = new SimpleXMLElement($rawbody);
+// TODO MDL-46023 Replace this code with a call to the new library.
+$origentity = libxml_disable_entity_loader(true);
+$xml = simplexml_load_string($rawbody);
+if (!$xml) {
+ libxml_disable_entity_loader($origentity);
+ throw new Exception('Invalid XML content');
+}
+libxml_disable_entity_loader($origentity);
$body = $xml->imsx_POXBody;
foreach ($body->children() as $child) {
View
12 notes/index.php
@@ -15,6 +15,10 @@
$filtertype = optional_param('filtertype', '', PARAM_ALPHA);
$filterselect = optional_param('filterselect', 0, PARAM_INT);
+if (empty($CFG->enablenotes)) {
+ print_error('notesdisabled', 'notes');
+}
+
$url = new moodle_url('/notes/index.php');
if ($courseid != SITEID) {
$url->param('course', $courseid);
@@ -61,11 +65,6 @@
/// require login to access notes
require_login($course);
-add_to_log($courseid, 'notes', 'view', 'index.php?course='.$courseid.'&amp;user='.$userid, 'view notes');
-
-if (empty($CFG->enablenotes)) {
- print_error('notesdisabled', 'notes');
-}
/// output HTML
if ($course->id == SITEID) {
@@ -73,8 +72,11 @@
} else {
$coursecontext = context_course::instance($course->id); // Course context
}
+require_capability('moodle/notes:view', $coursecontext);
$systemcontext = context_system::instance(); // SYSTEM context
+add_to_log($courseid, 'notes', 'view', 'index.php?course='.$courseid.'&amp;user='.$userid, 'view notes');
+
$strnotes = get_string('notes', 'notes');
if ($userid) {
$PAGE->set_context(context_user::instance($user->id));
View
7 question/format.php
@@ -357,6 +357,7 @@ public function importprocess($category) {
$count = 0;
foreach ($questions as $question) { // Process and store each question
+ $transaction = $DB->start_delegated_transaction();
// reset the php timeout
set_time_limit(0);
@@ -371,6 +372,7 @@ public function importprocess($category) {
$this->category = $newcategory;
}
}
+ $transaction->allow_commit();
continue;
}
$question->context = $this->importcontext;
@@ -429,9 +431,14 @@ public function importprocess($category) {
if (!empty($result->error)) {
echo $OUTPUT->notification($result->error);
+ // Can't use $transaction->rollback(); since it requires an exception,
+ // and I don't want to rewrite this code to change the error handling now.
+ $DB->force_transaction_rollback();
return false;
}
+ $transaction->allow_commit();
+
if (!empty($result->notice)) {
echo $OUTPUT->notification($result->notice);
return true;
View
7 question/format/webct/format.php
@@ -606,12 +606,9 @@ public function readquestions ($lines) {
// Calculated Question.
$question = $this->defaultquestion();
$question->qtype = 'calculated';
- $question->answers = array(); // No problem as they go as :FORMULA: from webct.
+ $question->answer = array(); // No problem as they go as :FORMULA: from webct.
$question->units = array();
$question->dataset = array();
-
- // To make us pass the end-of-question sanity checks.
- $question->answer = array('dummy');
$question->fraction = array('1.0');
$question->feedback = array();
@@ -736,7 +733,7 @@ public function readquestions ($lines) {
if (preg_match('~^:FORMULA:(.*)~i', $line, $webctoptions)) {
// Answer for a calculated question.
++$currentchoice;
- $question->answers[$currentchoice] =
+ $question->answer[$currentchoice] =
qformat_webct_convert_formula($webctoptions[1]);
// Default settings.
View
4 question/format/xml/format.php
@@ -774,7 +774,7 @@ public function import_calculated($question) {
// get answers array
$answers = $question['#']['answer'];
- $qo->answers = array();
+ $qo->answer = array();
$qo->feedback = array();
$qo->fraction = array();
$qo->tolerance = array();
@@ -788,7 +788,7 @@ public function import_calculated($question) {
if (empty($ans->answer['text'])) {
$ans->answer['text'] = '*';
}
- $qo->answers[] = $ans->answer;
+ $qo->answer[] = $ans->answer['text'];
$qo->feedback[] = $ans->feedback;
$qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
// fraction as a tag is deprecated
View
45 question/type/calculated/edit_calculated_form.php
@@ -189,36 +189,39 @@ public function qtype() {
return 'calculated';
}
- public function validation($data, $files) {
-
- // verifying for errors in {=...} in question text;
- $qtext = "";
- $qtextremaining = $data['questiontext']['text'];
- $possibledatasets = $this->qtypeobj->find_dataset_names($data['questiontext']['text']);
- foreach ($possibledatasets as $name => $value) {
- $qtextremaining = str_replace('{'.$name.'}', '1', $qtextremaining);
- }
- while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
- $qtextsplits = explode($regs1[0], $qtextremaining, 2);
- $qtext = $qtext.$qtextsplits[0];
- $qtextremaining = $qtextsplits[1];
- if (!empty($regs1[1]) && $formulaerrors =
- qtype_calculated_find_formula_errors($regs1[1])) {
- if (!isset($errors['questiontext'])) {
- $errors['questiontext'] = $formulaerrors.':'.$regs1[1];
- } else {
- $errors['questiontext'] .= '<br/>'.$formulaerrors.':'.$regs1[1];
- }
- }
+ /**
+ * Validate the equations in the some question content.
+ * @param array $errors where errors are being accumulated.
+ * @param string $field the field being validated.
+ * @param string $text the content of that field.
+ * @return array the updated $errors array.
+ */
+ protected function validate_text($errors, $field, $text) {
+ $problems = qtype_calculated_find_formula_errors_in_text($text);
+ if ($problems) {
+ $errors[$field] = $problems;
}
+ return $errors;
+ }
+ public function validation($data, $files) {
$errors = parent::validation($data, $files);
+ // Verifying for errors in {=...} in question text.
+ $errors = $this->validate_text($errors, 'questiontext', $data['questiontext']['text']);
+ $errors = $this->validate_text($errors, 'generalfeedback', $data['generalfeedback']['text']);
+
// Check that the answers use datasets.
$answers = $data['answer'];
$mandatorydatasets = array();
foreach ($answers as $key => $answer) {
+ $problems = qtype_calculated_find_formula_errors($answer);
+ if ($problems) {
+ $errors['answer['.$key.']'] = $problems;
+ }
$mandatorydatasets += $this->qtypeobj->find_dataset_names($answer);
+ $errors = $this->validate_text($errors, 'feedback[' . $key . ']',
+ $data['feedback'][$key]['text']);
}
if (empty($mandatorydatasets)) {
foreach ($answers as $key => $answer) {
View
108 question/type/calculated/question.php
@@ -259,6 +259,7 @@ public function datasets_are_synchronised($category) {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculated_variable_substituter {
+
/** @var array variable name => value */
protected $values;
@@ -341,6 +342,10 @@ public function get_values() {
* @return float the computed result.
*/
public function calculate($expression) {
+ // Make sure no malicious code is present in the expression. Refer MDL-46148 for details.
+ if ($error = qtype_calculated_find_formula_errors($expression)) {
+ throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', $error);
+ }
return $this->calculate_raw($this->substitute_values_for_eval($expression));
}
@@ -388,108 +393,11 @@ protected function substitute_values_pretty($text) {
* @return string the text with values substituted.
*/
public function replace_expressions_in_text($text, $length = null, $format = null) {
- $vs = $this; // Can't see to use $this in a PHP closure.
- $text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~',
+ $vs = $this; // Can't use $this in a PHP closure.
+ $text = preg_replace_callback(qtype_calculated::FORMULAS_IN_TEXT_REGEX,
function ($matches) use ($vs, $format, $length) {
return $vs->format_float($vs->calculate($matches[1]), $length, $format);
}, $text);
return $this->substitute_values_pretty($text);
}
-
- /**
- * Return an array describing any problems there are with an expression.
- * Returns false if the expression is fine.
- * @param string $formula an expression.
- * @return array|false list of problems, or false if the exression is OK.
- */
- public function get_formula_errors($formula) {
- // Validates the formula submitted from the question edit page.
- // Returns false if everything is alright.
- // Otherwise it constructs an error message
- // Strip away dataset names
- while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
- $formula = str_replace($regs[0], '1', $formula);
- }
-
- // Strip away empty space and lowercase it
- $formula = strtolower(str_replace(' ', '', $formula));
-
- $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
- $operatorornumber = "[$safeoperatorchar.0-9eE]";
-
- while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" .
- "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
- $formula, $regs)) {
- switch ($regs[2]) {
- // Simple parenthesis
- case '':
- if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
- return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
- }
- break;
-
- // Zero argument functions
- case 'pi':
- if ($regs[3]) {
- return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
- }
- break;
-
- // Single argument functions (the most common case)
- case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
- case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
- case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
- case 'exp': case 'expm1': case 'floor': case 'is_finite':
- case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
- case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
- case 'tan': case 'tanh':
- if (!empty($regs[4]) || empty($regs[3])) {
- return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]);
- }
- break;
-
- // Functions that take one or two arguments
- case 'log': case 'round':
- if (!empty($regs[5]) || empty($regs[3])) {
- return get_string('functiontakesoneortwoargs', 'qtype_calculated',
- $regs[2]);
- }
- break;
-
- // Functions that must have two arguments
- case 'atan2': case 'fmod': case 'pow':
- if (!empty($regs[5]) || empty($regs[4])) {
- return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
- }
- break;
-
- // Functions that take two or more arguments
- case 'min': case 'max':
- if (empty($regs[4])) {
- return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
- }
- break;
-
- default:
- return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
- }
-
- // Exchange the function call with '1' and then chack for
- // another function call...
- if ($regs[1]) {
- // The function call is proceeded by an operator
- $formula = str_replace($regs[0], $regs[1] . '1', $formula);
- } else {
- // The function call starts the formula
- $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
- }
- }
-
- if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
- return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
- } else {
- // Formula just might be valid
- return false;
- }
- }
-}
+}
View
168 question/type/calculated/questiontype.php
@@ -37,6 +37,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculated extends question_type {
+ /** Regular expression that finds the formulas in content. */
+ const FORMULAS_IN_TEXT_REGEX = '~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)\}~';
+
const MAX_DATASET_ITEMS = 100;
public $wizardpagesnumber = 3;
@@ -128,12 +131,14 @@ public function get_datasets_for_export($question) {
public function save_question_options($question) {
global $CFG, $DB;
- // the code is used for calculated, calculatedsimple and calculatedmulti qtypes
+
+ // Make it impossible to save bad formulas anywhere.
+ $this->validate_question_data($question);
+
+ // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
$context = $question->context;
- if (isset($question->answer) && !isset($question->answers)) {
- $question->answers = $question->answer;
- }
- // calculated options
+
+ // Calculated options.
$update = true;
$options = $DB->get_record('question_calculated_options',
array('question' => $question->id));
@@ -182,14 +187,7 @@ public function save_question_options($question) {
$units = $result->units;
}
- // Insert all the new answers
- if (isset($question->answer) && !isset($question->answers)) {
- $question->answers = $question->answer;
- }
- foreach ($question->answers as $key => $answerdata) {
- if (is_array($answerdata)) {
- $answerdata = $answerdata['text'];
- }
+ foreach ($question->answer as $key => $answerdata) {
if (trim($answerdata) == '') {
continue;
}
@@ -343,49 +341,6 @@ protected function initialise_question_instance(question_definition $question, $
$question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
}
- public function validate_form($form) {
- switch($form->wizardpage) {
- case 'question':
- $calculatedmessages = array();
- if (empty($form->name)) {
- $calculatedmessages[] = get_string('missingname', 'qtype_calculated');
- }
- if (empty($form->questiontext)) {
- $calculatedmessages[] = get_string('missingquestiontext', 'qtype_calculated');
- }
- // Verify formulas
- foreach ($form->answers as $key => $answer) {
- if ('' === trim($answer)) {
- $calculatedmessages[] = get_string(
- 'missingformula', 'qtype_calculated');
- }
- if ($formulaerrors = qtype_calculated_find_formula_errors($answer)) {
- $calculatedmessages[] = $formulaerrors;
- }
- if (! isset($form->tolerance[$key])) {
- $form->tolerance[$key] = 0.0;
- }
- if (! is_numeric($form->tolerance[$key])) {
- $calculatedmessages[] = get_string(
- 'tolerancemustbenumeric', 'qtype_calculated');
- }
- }
-
- if (!empty($calculatedmessages)) {
- $errorstring = "The following errors were found:<br />";
- foreach ($calculatedmessages as $msg) {
- $errorstring .= $msg . '<br />';
- }
- print_error($errorstring);
- }
-
- break;
- default:
- return parent::validate_form($form);
- break;
- }
- return true;
- }
public function finished_edit_wizard($form) {
return isset($form->savechanges);
}
@@ -482,6 +437,55 @@ public function display_question_editing_page($mform, $question, $wizardnow) {
}
/**
+ * Verify that the equations in part of the question are OK.
+ * We throw an exception here because this should have already been validated
+ * by the form. This is just a last line of defence to prevent a question
+ * being stored in the database if it has bad formulas. This saves us from,
+ * for example, malicious imports.
+ * @param string $text containing equations.
+ */
+ protected function validate_text($text) {
+ $error = qtype_calculated_find_formula_errors_in_text($text);
+ if ($error) {
+ throw new coding_exception($error);
+ }
+ }
+
+ /**
+ * Verify that an answer is OK.
+ * We throw an exception here because this should have already been validated
+ * by the form. This is just a last line of defence to prevent a question
+ * being stored in the database if it has bad formulas. This saves us from,
+ * for example, malicious imports.
+ * @param string $text containing equations.
+ */
+ protected function validate_answer($answer) {
+ $error = qtype_calculated_find_formula_errors($answer);
+ if ($error) {
+ throw new coding_exception($error);
+ }
+ }
+
+ /**
+ * Validate data before save.
+ * @param stdClass $question data from the form / import file.
+ */
+ protected function validate_question_data($question) {
+ $this->validate_text($question->questiontext); // Yes, really no ['text'].
+
+ if (isset($question->generalfeedback['text'])) {
+ $this->validate_text($question->generalfeedback['text']);
+ } else if (isset($question->generalfeedback)) {
+ $this->validate_text($question->generalfeedback); // Because question import is weird.
+ }
+
+ foreach ($question->answer as $key => $answer) {
+ $this->validate_answer($answer);
+ $this->validate_text($question->feedback[$key]['text']);
+ }
+ }
+
+ /**
* This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
* so that they can be saved
* using the function save_dataset_definitions($form)
@@ -493,12 +497,13 @@ public function display_question_editing_page($mform, $question, $wizardnow) {
* @param object $form
* @param int $questionfromid default = '0'
*/
- public function preparedatasets($form , $questionfromid = '0') {
- // the dataset names present in the edit_question_form and edit_calculated_form
- // are retrieved
+ public function preparedatasets($form, $questionfromid = '0') {
+
+ // The dataset names present in the edit_question_form and edit_calculated_form
+ // are retrieved.
$possibledatasets = $this->find_dataset_names($form->questiontext);
$mandatorydatasets = array();
- foreach ($form->answers as $answer) {
+ foreach ($form->answer as $key => $answer) {
$mandatorydatasets += $this->find_dataset_names($answer);
}
// if there are identical datasetdefs already saved in the original question.
@@ -587,8 +592,9 @@ public function addnamecategory(&$question) {
*/
public function save_question($question, $form) {
global $DB;
+
if ($this->wizardpagesnumber() == 1 || $question->qtype == 'calculatedsimple') {
- $question = parent::save_question($question, $form);
+ $question = parent::save_question($question, $form);
return $question;
}
@@ -605,8 +611,8 @@ public function save_question($question, $form) {
// See where we're coming from
switch($wizardnow) {
case '' :
- case 'question': // coming from the first page, creating the second
- if (empty($form->id)) { // for a new question $form->id is empty
+ case 'question': // Coming from the first page, creating the second.
+ if (empty($form->id)) { // or a new question $form->id is empty.
$question = parent::save_question($question, $form);
//prepare the datasets using default $questionfromid
$this->preparedatasets($form);
@@ -1057,10 +1063,14 @@ public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
}
$answers = fullclone($answers);
- $errors = '';
$delimiter = ': ';
$virtualqtype = $qtypeobj->get_virtual_qtype();
foreach ($answers as $key => $answer) {
+ $error = qtype_calculated_find_formula_errors($answer->answer);
+ if ($error) {
+ $comment->stranswers[$key] = $error;
+ continue;
+ }
$formula = $this->substitute_variables($answer->answer, $data);
$formattedanswer = qtype_calculated_calculate_answer(
$answer->answer, $data, $answer->tolerance,
@@ -1973,6 +1983,11 @@ function qtype_calculated_calculate_answer($formula, $individualdata,
}
+/**
+ * Validate a forumula.
+ * @param string $formula the formula to validate.
+ * @return string|boolean false if there are no problems. Otherwise a string error message.
+ */
function qtype_calculated_find_formula_errors($formula) {
// Validates the formula submitted from the question edit page.
// Returns false if everything is alright.
@@ -2001,7 +2016,7 @@ function qtype_calculated_find_formula_errors($formula) {
// Zero argument functions
case 'pi':
- if ($regs[3]) {
+ if (array_key_exists(3, $regs)) {
return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
}
break;
@@ -2062,3 +2077,26 @@ function qtype_calculated_find_formula_errors($formula) {
return false;
}
}
+
+/**
+ * Validate all the forumulas in a bit of text.
+ * @param string $text the text in which to validate the formulas.
+ * @return string|boolean false if there are no problems. Otherwise a string error message.
+ */
+function qtype_calculated_find_formula_errors_in_text($text) {
+ preg_match_all(qtype_calculated::FORMULAS_IN_TEXT_REGEX, $text, $matches);
+
+ $errors = array();
+ foreach ($matches[1] as $match) {
+ $error = qtype_calculated_find_formula_errors($match);
+ if ($error) {
+ $errors[] = $error;
+ }
+ }
+
+ if ($errors) {
+ return implode(' ', $errors);
+ }
+
+ return false;
+}
View
105 question/type/calculated/tests/formula_validation_test.php
@@ -0,0 +1,105 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for formula validation code.
+ *
+ * @package qtype_calculated
+ * @copyright 2014 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/question/type/calculated/questiontype.php');
+
+
+/**
+ * Unit tests for formula validation code.
+ *
+ * @copyright 2014 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_calculated_formula_validation_testcase extends basic_testcase {
+ protected function assert_nonempty_string($actual) {
+ $this->assertInternalType('string', $actual);
+ $this->assertNotEquals('', $actual);
+ }
+
+ public function test_simple_equations_ok() {
+ $this->assertFalse(qtype_calculated_find_formula_errors(1));
+ $this->assertFalse(qtype_calculated_find_formula_errors('1 + 1'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('{x} + {y}'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('{x}*{y}'));
+ }
+
+ public function test_safe_functions_ok() {
+ $this->assertFalse(qtype_calculated_find_formula_errors('abs(-1)'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('tan(pi())'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('log(10)'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('log(64, 2)'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('atan2(1.0, 1.0)'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('max(1.0, 1.0)'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('max(1.0, 1.0, 2.0)'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('max(1.0, 1.0, 2, 3)'));
+ }
+
+ public function test_dangerous_functions_blocked() {
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('eval(1)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('system(1)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('base64_decode(1)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('unserialize(1)'));
+
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('cos(tan(1) + abs(cos(eval)) * pi())'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('eval (CONSTANTREADASSTRING)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors("eval \t ()"));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('"eval"()'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('?><?php()'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('?><?php+1'));
+ }
+
+ public function test_functions_with_wrong_num_args_caught() {
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('abs(-1, 1)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('abs()'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('pi(1)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('log()'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('log(64, 2, 3)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('atan2(1.0)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('atan2(1.0, 1.0, 2.0)'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('max(1.0)'));
+ }
+
+ public function test_validation_of_formulas_in_text_ok() {
+ $this->assertFalse(qtype_calculated_find_formula_errors_in_text(
+ '<p>Look no equations.</p>'));
+ $this->assertFalse(qtype_calculated_find_formula_errors_in_text(
+ '<p>Simple variable: {x}.</p>'));
+ $this->assertFalse(qtype_calculated_find_formula_errors_in_text(
+ '<p>This is an equation: {=1+1}, as is this: {={x}+{y}}.</p>' .
+ '<p>Here is a more complex one: {=sin(2*pi()*{theta})}.</p>'));
+ }
+
+ public function test_validation_of_formulas_in_text_bad_function() {
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors_in_text(
+ '<p>This is an equation: {=eval(1)}.</p>'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors_in_text(
+ '<p>Good: {=1+1}, bad: {=eval(1)}, good: {={x}+{y}}.</p>'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors_in_text(
+ '<p>Bad: {=eval(1)}, bad: {=system(1)}.</p>'));
+ }
+}
View
66 question/type/calculatedmulti/edit_calculatedmulti_form.php
@@ -218,30 +218,31 @@ protected function data_preprocessing_answers($question, $withanswerfiles = fals
return $question;
}
+ /**
+ * Validate the equations in the some question content.
+ * @param array $errors where errors are being accumulated.
+ * @param string $field the field being validated.
+ * @param string $text the content of that field.
+ * @return array the updated $errors array.
+ */
+ protected function validate_text($errors, $field, $text) {
+ $problems = qtype_calculated_find_formula_errors_in_text($text);
+ if ($problems) {
+ $errors[$field] = $problems;
+ }
+ return $errors;
+ }
+
public function validation($data, $files) {
$errors = parent::validation($data, $files);
//verifying for errors in {=...} in question text;
- $qtext = '';
- $qtextremaining = $data['questiontext']['text'];
- $possibledatasets = $this->qtypeobj->find_dataset_names($data['questiontext']['text']);
- foreach ($possibledatasets as $name => $value) {
- $qtextremaining = str_replace('{'.$name.'}', '1', $qtextremaining);
- }
+ $errors = $this->validate_text($errors, 'questiontext', $data['questiontext']['text']);
+ $errors = $this->validate_text($errors, 'generalfeedback', $data['generalfeedback']['text']);
+ $errors = $this->validate_text($errors, 'correctfeedback', $data['correctfeedback']['text']);
+ $errors = $this->validate_text($errors, 'partiallycorrectfeedback', $data['partiallycorrectfeedback']['text']);
+ $errors = $this->validate_text($errors, 'incorrectfeedback', $data['incorrectfeedback']['text']);
- while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
- $qtextsplits = explode($regs1[0], $qtextremaining, 2);
- $qtext = $qtext.$qtextsplits[0];
- $qtextremaining = $qtextsplits[1];
- if (!empty($regs1[1]) && $formulaerrors =
- qtype_calculated_find_formula_errors($regs1[1])) {
- if (!isset($errors['questiontext'])) {
- $errors['questiontext'] = $formulaerrors.':'.$regs1[1];
- } else {
- $errors['questiontext'] .= '<br/>'.$formulaerrors.':'.$regs1[1];
- }
- }
- }
$answers = $data['answer'];
$answercount = 0;
$maxgrade = false;
@@ -256,6 +257,7 @@ public function validation($data, $files) {
get_string('atleastonewildcard', 'qtype_calculated');
}
}
+
$totalfraction = 0;
$maxfraction = -1;
foreach ($answers as $key => $answer) {
@@ -268,28 +270,12 @@ public function validation($data, $files) {
$errors['fraction['.$key.']'] = get_string('errgradesetanswerblank', 'qtype_multichoice');
}
if ($trimmedanswer != '' || $answercount == 0) {
- //verifying for errors in {=...} in answer text;
- $qanswer = '';
- $qanswerremaining = $trimmedanswer;
- $possibledatasets = $this->qtypeobj->find_dataset_names($trimmedanswer);
- foreach ($possibledatasets as $name => $value) {
- $qanswerremaining = str_replace('{'.$name.'}', '1', $qanswerremaining);
- }
-
- while (preg_match('~\{=([^[:space:]}]*)}~', $qanswerremaining, $regs1)) {
- $qanswersplits = explode($regs1[0], $qanswerremaining, 2);
- $qanswer = $qanswer . $qanswersplits[0];
- $qanswerremaining = $qanswersplits[1];
- if (!empty($regs1[1]) && $formulaerrors =
- qtype_calculated_find_formula_errors($regs1[1])) {
- if (!isset($errors['answer['.$key.']'])) {
- $errors['answer['.$key.']'] = $formulaerrors.':'.$regs1[1];
- } else {
- $errors['answer['.$key.']'] .= '<br/>'.$formulaerrors.':'.$regs1[1];
- }
- }
- }
+ // Verifying for errors in {=...} in answer text.
+ $errors = $this->validate_text($errors, 'answeroptions[' . $key . ']', $answer);
+ $errors = $this->validate_text($errors, 'feedback[' . $key . ']',
+ $data['feedback'][$key]['text']);
}
+
if ($trimmedanswer != '') {
if ('2' == $data['correctanswerformat'][$key] &&
'0' == $data['correctanswerlength'][$key]) {
View
26 question/type/calculatedmulti/questiontype.php
@@ -42,7 +42,10 @@ public function save_question_options($question) {
global $CFG, $DB;
$context = $question->context;
- // Calculated options
+ // Make it impossible to save bad formulas anywhere.
+ $this->validate_question_data($question);
+
+ // Calculated options.
$update = true;
$options = $DB->get_record('question_calculated_options',
array('question' => $question->id));
@@ -71,11 +74,8 @@ public function save_question_options($question) {
$oldoptions = array();
}
- // Insert all the new answers
- if (isset($question->answer) && !isset($question->answers)) {
- $question->answers = $question->answer;
- }
- foreach ($question->answers as $key => $answerdata) {
+ // Insert all the new answers.
+ foreach ($question->answer as $key => $answerdata) {
if (is_array($answerdata)) {
$answerdata = $answerdata['text'];
}
@@ -156,6 +156,20 @@ public function save_question_options($question) {
return true;
}
+ protected function validate_answer($answer) {
+ $error = qtype_calculated_find_formula_errors_in_text($answer);
+ if ($error) {
+ throw new coding_exception($error);
+ }
+ }
+
+ protected function validate_question_data($question) {
+ parent::validate_question_data($question);
+ $this->validate_text($question->correctfeedback['text']);
+ $this->validate_text($question->partiallycorrectfeedback['text']);
+ $this->validate_text($question->incorrectfeedback['text']);
+ }
+
protected function make_question_instance($questiondata) {
question_bank::load_question_definition_classes($this->name());
if ($questiondata->options->single) {
View
13 question/type/calculatedsimple/questiontype.php
@@ -43,11 +43,9 @@ class qtype_calculatedsimple extends qtype_calculated {
public function save_question_options($question) {
global $CFG, $DB;
$context = $question->context;
- // Get old answers:
- if (isset($question->answer) && !isset($question->answers)) {
- $question->answers = $question->answer;
- }
+ // Make it impossible to save bad formulas anywhere.
+ $this->validate_question_data($question);
// Get old versions of the objects
if (!$oldanswers = $DB->get_records('question_answers',
@@ -68,11 +66,8 @@ public function save_question_options($question) {
} else {
$units = &$result->units;
}
- // Insert all the new answers
- if (isset($question->answer) && !isset($question->answers)) {
- $question->answers = $question->answer;
- }
- foreach ($question->answers as $key => $answerdata) {
+ // Insert all the new answers.
+ foreach ($question->answer as $key => $answerdata) {
if (is_array($answerdata)) {
$answerdata = $answerdata['text'];
}
View
10 repository/coursefiles/lib.php
@@ -63,7 +63,7 @@ public function get_listing($encodedpath = '', $page = '') {
$browser = get_file_browser();
if (!empty($encodedpath)) {
- $params = unserialize(base64_decode($encodedpath));
+ $params = json_decode(base64_decode($encodedpath), true);
if (is_array($params)) {
$filepath = is_null($params['filepath']) ? NULL : clean_param($params['filepath'], PARAM_PATH);;
$filename = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE);
@@ -80,12 +80,12 @@ public function get_listing($encodedpath = '', $page = '') {
if ($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename)) {
// build path navigation
$pathnodes = array();
- $encodedpath = base64_encode(serialize($fileinfo->get_params()));
+ $encodedpath = base64_encode(json_encode($fileinfo->get_params()));
$pathnodes[] = array('name'=>$fileinfo->get_visible_name(), 'path'=>$encodedpath);
$level = $fileinfo->get_parent();
while ($level) {
$params = $level->get_params();
- $encodedpath = base64_encode(serialize($params));
+ $encodedpath = base64_encode(json_encode($params));
if ($params['contextid'] != $context->id) {
break;
}
@@ -102,7 +102,7 @@ public function get_listing($encodedpath = '', $page = '') {
if ($child->is_directory()) {
$params = $child->get_params();
$subdir_children = $child->get_children();
- $encodedpath = base64_encode(serialize($params));
+ $encodedpath = base64_encode(json_encode($params));
$node = array(
'title' => $child->get_visible_name(),
'datemodified' => $child->get_timemodified(),
@@ -113,7 +113,7 @@ public function get_listing($encodedpath = '', $page = '') {
);
$list[] = $node;
} else {
- $encodedpath = base64_encode(serialize($child->get_params()));
+ $encodedpath = base64_encode(json_encode($child->get_params()));
$node = array(
'title' => $child->get_visible_name(),
'size' => $child->get_filesize(),
View
2  repository/equella/callback.php
@@ -55,7 +55,7 @@
$license = s(clean_param($info->license, PARAM_ALPHAEXT));
}
-$source = base64_encode(serialize((object)array('url'=>$url,'filename'=>$filename)));
+$source = base64_encode(json_encode(array('url'=>$url,'filename'=>$filename)));
$js =<<<EOD
<html>
View
15 repository/equella/lib.php
@@ -119,7 +119,11 @@ public function supported_returntypes() {
* @return string file referece
*/
public function get_file_reference($source) {
- return $source;
+ // Internally we store serialized value but user input is json-encoded for security reasons.
+ $ref = json_decode(base64_decode($source));
+ $filename = clean_param($ref->filename, PARAM_FILE);
+ $url = clean_param($ref->url, PARAM_URL);
+ return base64_encode(serialize((object)array('url' => $url, 'filename' => $filename)));
}
/**
@@ -407,12 +411,13 @@ private static function to_mime_type($value) {
/**
* Return the source information
*
- * @param stdClass $url
+ * @param string $source
* @return string|null
*/
- public function get_file_source_info($url) {
- $ref = unserialize(base64_decode($url));
- return 'EQUELLA: ' . $ref->filename;
+ public function get_file_source_info($source) {
+ $ref = json_decode(base64_decode($source));
+ $filename = clean_param($ref->filename, PARAM_FILE);
+ return 'EQUELLA: ' . $filename;
}
/**
View
2  repository/filepicker.php
@@ -293,7 +293,7 @@
// note that in this case user may not have permission to access the source file directly
// so no file_browser/file_info can be used below
if ($repo->has_moodle_files()) {
- $file = repository::get_moodle_file($fileurl);
+ $file = repository::get_moodle_file($reference);
if ($file && $file->is_external_file()) {
$sourcefield = $file->get_source(); // remember the original source
$record->source = $repo::build_source_field($sourcefield);
View
49 repository/lib.php
@@ -717,13 +717,14 @@ public static function draftfile_exists($itemid, $filepath, $filename) {
}
/**
- * Parses the 'source' returned by moodle repositories and returns an instance of stored_file
+ * Parses the moodle file reference and returns an instance of stored_file
*
- * @param string $source
+ * @param string $reference reference to the moodle internal file as retruned by
+ * {@link repository::get_file_reference()} or {@link file_storage::pack_reference()}
* @return stored_file|null
*/
- public static function get_moodle_file($source) {
- $params = file_storage::unpack_reference($source, true);
+ public static function get_moodle_file($reference) {
+ $params = file_storage::unpack_reference($reference, true);
$fs = get_file_storage();
return $fs->get_file($params['contextid'], $params['component'], $params['filearea'],
$params['itemid'], $params['filepath'], $params['filename']);
@@ -735,13 +736,14 @@ public static function get_moodle_file($source) {
* This is checked when user tries to pick the file from repository to deal with
* potential parameter substitutions is request
*
- * @param string $source
+ * @param string $source source of the file, returned by repository as 'source' and received back from user (not cleaned)
* @return bool whether the file is accessible by current user
*/
public function file_is_accessible($source) {
if ($this->has_moodle_files()) {
+ $reference = $this->get_file_reference($source);
try {
- $params = file_storage::unpack_reference($source, true);
+ $params = file_storage::unpack_reference($reference, true);
} catch (file_reference_exception $e) {
return false;
}
@@ -1336,12 +1338,13 @@ public function get_file_by_reference($reference) {
* again to another file area (also as a copy or as a reference), the value of
* files.source is copied.
*
- * @param string $source the value that repository returned in listing as 'source'
+ * @param string $source source of the file, returned by repository as 'source' and received back from user (not cleaned)
* @return string|null
*/
public function get_file_source_info($source) {
if ($this->has_moodle_files()) {
- return $this->get_reference_details($source, 0);
+ $reference = $this->get_file_reference($source);
+ return $this->get_reference_details($reference, 0);
}
return $source;
}
@@ -1621,13 +1624,33 @@ public static function display_instances_list($context, $typename = null) {
/**
* Prepare file reference information
*
- * @param string $source
- * @return string file referece
+ * @param string $source source of the file, returned by repository as 'source' and received back from user (not cleaned)
+ * @return string file reference, ready to be stored
*/
public function get_file_reference($source) {
- if ($this->has_moodle_files() && ($this->supported_returntypes() & FILE_REFERENCE)) {
- $params = file_storage::unpack_reference($source);
- if (!is_array($params)) {
+ if ($source && $this->has_moodle_files()) {
+ $params = @json_decode(base64_decode($source), true);
+ if (!$params && !in_array($this->get_typename(), array('recent', 'user', 'local', 'coursefiles'))) {
+ // IMPORTANT! Since default format for moodle files was changed in the minor release as a security fix
+ // we maintain an old code here in order not to break 3rd party repositories that deal
+ // with moodle files. Repositories are strongly encouraged to be upgraded, see MDL-45616.
+ // In Moodle 2.8 this fallback will be removed.
+ $params = file_storage::unpack_reference($source, true);
+ return file_storage::pack_reference($params);
+ }
+ if (!is_array($params) || empty($params['contextid'])) {
+ throw new repository_exception('invalidparams', 'repository');
+ }
+ $params = array(
+ 'component' => empty($params['component']) ? '' : clean_param($params['component'], PARAM_COMPONENT),
+ 'filearea' => empty($params['filearea']) ? '' : clean_param($params['filearea'], PARAM_AREA),
+ 'itemid' => empty($params['itemid']) ? 0 : clean_param($params['itemid'], PARAM_INT),
+ 'filename' => empty($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE),
+ 'filepath' => empty($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH),
+ 'contextid' => clean_param($params['contextid'], PARAM_INT)
+ );
+ // Check if context exists.
+ if (!context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
throw new repository_exception('invalidparams', 'repository');
}
return file_storage::pack_reference($params);
View
6 repository/local/lib.php
@@ -56,7 +56,7 @@ public function get_listing($encodedpath = '', $page = '') {
$component = null;
if (!empty($encodedpath)) {
- $params = unserialize(base64_decode($encodedpath));
+ $params = json_decode(base64_decode($encodedpath), true);
if (is_array($params) && isset($params['contextid'])) {
$component = is_null($params['component']) ? NULL : clean_param($params['component'], PARAM_COMPONENT);
$filearea = is_null($params['filearea']) ? NULL : clean_param($params['filearea'], PARAM_AREA);
@@ -216,7 +216,7 @@ private function can_skip(file_info $fileinfo, $extensions, $parent = -1) {
*/
private function get_node(file_info $fileinfo) {
global $OUTPUT;
- $encodedpath = base64_encode(serialize($fileinfo->get_params()));
+ $encodedpath = base64_encode(json_encode($fileinfo->get_params()));
$node = array(
'title' => $fileinfo->get_visible_name(),
'datemodified' => $fileinfo->get_timemodified(),
@@ -256,7 +256,7 @@ private function get_node(file_info $fileinfo) {
* @return array
*/
private function get_node_path(file_info $fileinfo) {
- $encodedpath = base64_encode(serialize($fileinfo->get_params()));
+ $encodedpath = base64_encode(json_encode($fileinfo->get_params()));
return array(
'path' => $encodedpath,
'name' => $fileinfo->get_visible_name()
View
5 repository/recent/lib.php
@@ -126,7 +126,7 @@ public function get_listing($encodedpath = '', $page = '') {
$fileinfo = $browser->get_file_info($context, $file['component'],
$file['filearea'], $file['itemid'], $file['filepath'], $file['filename']);
if ($fileinfo) {
- $params = base64_encode(serialize($file));
+ $params = base64_encode(json_encode($file));
$node = array(
'title' => $fileinfo->get_visible_name(),
'size' => $fileinfo->get_filesize(),
@@ -191,7 +191,8 @@ public function supported_returntypes() {
*/
public function file_is_accessible($source) {
global $USER;
- $file = self::get_moodle_file($source);
+ $reference = $this->get_file_reference($source);
+ $file = self::get_moodle_file($reference);
return (!empty($file) && $file->get_userid() == $USER->id);
}
View
2  repository/repository_ajax.php
@@ -239,7 +239,7 @@
// note that in this case user may not have permission to access the source file directly
// so no file_browser/file_info can be used below
if ($repo->has_moodle_files()) {
- $file = repository::get_moodle_file($source);
+ $file = repository::get_moodle_file($reference);
if ($file && $file->is_external_file()) {
$sourcefield = $file->get_source(); // remember the original source
$record->source = $repo::build_source_field($sourcefield);
View
8 repository/user/lib.php
@@ -61,7 +61,7 @@ public function get_listing($encodedpath = '', $page = '') {
$list = array();
if (!empty($encodedpath)) {
- $params = unserialize(base64_decode($encodedpath));
+ $params = json_decode(base64_decode($encodedpath), true);
if (is_array($params)) {
$filepath = clean_param($params['filepath'], PARAM_PATH);;
$filename = clean_param($params['filename'], PARAM_FILE);
@@ -84,7 +84,7 @@ public function get_listing($encodedpath = '', $page = '') {
$level = $fileinfo;
$params = $fileinfo->get_params();
while ($level && $params['component'] == 'user' && $params['filearea'] == 'private') {
- $encodedpath = base64_encode(serialize($level->get_params()));
+ $encodedpath = base64_encode(json_encode($level->get_params()));
$pathnodes[] = array('name'=>$level->get_visible_name(), 'path'=>$encodedpath);
$level = $level->get_parent();
$params = $level->get_params();
@@ -95,7 +95,7 @@ public function get_listing($encodedpath = '', $page = '') {
$children = $fileinfo->get_children();
foreach ($children as $child) {
if ($child->is_directory()) {
- $encodedpath = base64_encode(serialize($child->get_params()));
+ $encodedpath = base64_encode(json_encode($child->get_params()));
$node = array(
'title' => $child->get_visible_name(),
'datemodified' => $child->get_timemodified(),
@@ -106,7 +106,7 @@ public function get_listing($encodedpath = '', $page = '') {
);
$list[] = $node;
} else {
- $encodedpath = base64_encode(serialize($child->get_params()));
+ $encodedpath = base64_encode(json_encode($child->get_params()));
$node = array(
'title' => $child->get_visible_name(),
'size' => $child->get_filesize(),
Please sign in to comment.
Something went wrong with that request. Please try again.