MDL-41819 try to work around max_input_vars restriction

This commit is contained in:
Petr Škoda 2013-11-18 10:50:26 +08:00
parent 9b37cd72a2
commit a377754770
8 changed files with 199 additions and 71 deletions

View File

@ -73,6 +73,11 @@ abstract class base_moodleform extends moodleform {
*/
function __construct(base_ui_stage $uistage, $action=null, $customdata=null, $method='post', $target='', $attributes=null, $editable=true) {
$this->uistage = $uistage;
$attributes = (array)$attributes;
if (!isset($attributes['enctype'])) {
$attributes['enctype'] = 'application/x-www-form-urlencoded'; // Enforce compatibility with our max_input_vars hack.
}
parent::__construct($action, $customdata, $method, $target, $attributes, $editable);
}
/**

View File

@ -846,6 +846,7 @@ class restore_ui_stage_process extends restore_ui_stage {
$html .= html_writer::start_tag('form', array(
'action' => $url->out_omit_querystring(),
'class' => 'backup-restore',
'enctype' => 'application/x-www-form-urlencoded', // Enforce compatibility with our max_input_vars hack.
'method' => 'post'));
foreach ($url->params() as $name => $value) {
$html .= html_writer::empty_tag('input', array(

View File

@ -156,7 +156,7 @@ $reporthtml = $report->get_grade_table();
// print submit button
if ($USER->gradeediting[$course->id] && ($report->get_pref('showquickfeedback') || $report->get_pref('quickgrading'))) {
echo '<form action="index.php" method="post">';
echo '<form action="index.php" enctype="application/x-www-form-urlencoded" method="post">'; // Enforce compatibility with our max_input_vars hack.
echo '<div>';
echo '<input type="hidden" value="'.s($courseid).'" name="id" />';
echo '<input type="hidden" value="'.sesskey().'" name="sesskey" />';

View File

@ -1651,51 +1651,10 @@ class grade_report_grader extends grade_report {
/**
* Returns the maximum number of students to be displayed on each page
*
* Takes into account the 'studentsperpage' user preference and the 'max_input_vars'
* PHP setting. Too many fields is only a problem when submitting grades but
* we respect 'max_input_vars' even when viewing grades to prevent students disappearing
* when toggling editing on and off.
*
* @return int The maximum number of students to display per page
*/
public function get_students_per_page() {
global $USER;
static $studentsperpage = null;
if ($studentsperpage === null) {
$originalstudentsperpage = $studentsperpage = $this->get_pref('studentsperpage');
// Will this number of students result in more fields that we are allowed?
$maxinputvars = ini_get('max_input_vars');
if ($maxinputvars !== false) {
// We can't do anything about there being more grade items than max_input_vars,
// but we can decrease number of students per page if there are >= max_input_vars
$fieldsperstudent = 0; // The number of fields output per student
if ($this->get_pref('quickgrading') || $this->get_pref('showquickfeedback')) {
// Each array (grade, feedback) will gain one element
$fieldsperstudent ++;
}
$fieldsrequired = $studentsperpage * $fieldsperstudent;
if ($fieldsrequired >= $maxinputvars) {
$studentsperpage = $maxinputvars - 1; // Subtract one to be on the safe side
if ($studentsperpage<1) {
// Make sure students per page doesn't fall below 1, though if your
// max_input_vars is only 1 you've got bigger problems!
$studentsperpage = 1;
}
$a = new stdClass();
$a->originalstudentsperpage = $originalstudentsperpage;
$a->studentsperpage = $studentsperpage;
$a->maxinputvars = $maxinputvars;
debugging(get_string('studentsperpagereduced', 'grades', $a));
}
}
}
return $studentsperpage;
return $this->get_pref('studentsperpage');
}
}

View File

@ -261,10 +261,11 @@ abstract class moodleform {
$submission = array();
if ($method == 'post') {
if (!empty($_POST)) {
$submission = $this->_get_post_params();
$submission = $_POST;
}
} else {
$submission = array_merge_recursive($_GET, $this->_get_post_params()); // Emulate handling of parameters in xxxx_param().
$submission = $_GET;
merge_query_params($submission, $_POST); // Emulate handling of parameters in xxxx_param().
}
// following trick is needed to enable proper sesskey checks when using GET forms
@ -284,34 +285,12 @@ abstract class moodleform {
}
/**
* Internal method. Gets all POST variables, bypassing max_input_vars limit if needed.
*
* @return array All POST variables as an array, in the same format as $_POST.
* Internal method - should not be used anywhere.
* @deprecated since 2.6
* @return array $_POST.
*/
protected function _get_post_params() {
$enctype = $this->_form->getAttribute('enctype');
$max = (int)ini_get('max_input_vars');
if (empty($max) || count($_POST, COUNT_RECURSIVE) < $max || (!empty($enctype) && $enctype == 'multipart/form-data')) {
return $_POST;
}
// Large POST request with enctype supported by php://input.
// Parse php://input in chunks to bypass max_input_vars limit, which also applies to parse_str().
$allvalues = array();
$values = array();
$str = file_get_contents("php://input");
$delim = '&';
$fun = create_function('$p', 'return implode("'.$delim.'", $p);');
$chunks = array_map($fun, array_chunk(explode($delim, $str), $max));
foreach ($chunks as $chunk) {
parse_str($chunk, $values);
$allvalues = array_merge_recursive($allvalues, $values);
}
return $allvalues;
return $_POST;
}
/**

View File

@ -793,6 +793,10 @@ if (!empty($CFG->profilingenabled)) {
profiling_start();
}
// Hack to get around max_input_vars restrictions,
// we need to do this after session init to have some basic DDoS protection.
workaround_max_input_vars();
// Process theme change in the URL.
if (!empty($CFG->allowthemechangeonurl) and !empty($_GET['theme'])) {
// we have to use _GET directly because we do not want this to interfere with _POST

View File

@ -933,6 +933,108 @@ function setup_get_remote_url() {
return $rurl;
}
/**
* Try to work around the 'max_input_vars' restriction if necessary.
*/
function workaround_max_input_vars() {
// Make sure this gets executed only once from lib/setup.php!
static $executed = false;
if ($executed) {
debugging('workaround_max_input_vars() must be called only once!');
return;
}
$executed = true;
if (!isset($_SERVER["CONTENT_TYPE"]) or strpos($_SERVER["CONTENT_TYPE"], 'multipart/form-data') !== false) {
// Not a post or 'multipart/form-data' which is not compatible with "php://input" reading.
return;
}
if (!isloggedin() or isguestuser()) {
// Only real users post huge forms.
return;
}
$max = (int)ini_get('max_input_vars');
if ($max <= 0) {
// Most probably PHP < 5.3.9 that does not implement this limit.
return;
}
if ($max >= 200000) {
// This value should be ok for all our forms, by setting it in php.ini
// admins may prevent any unexpected regressions caused by this hack.
// Note there is no need to worry about DDoS caused by making this limit very high
// because there are very many easier ways to DDoS any Moodle server.
return;
}
if (count($_POST, COUNT_RECURSIVE) < $max) {
return;
}
// Large POST request with enctype supported by php://input.
// Parse php://input in chunks to bypass max_input_vars limit, which also applies to parse_str().
$str = file_get_contents("php://input");
if ($str === false or $str === '') {
// Some weird error.
return;
}
$delim = '&';
$fun = create_function('$p', 'return implode("'.$delim.'", $p);');
$chunks = array_map($fun, array_chunk(explode($delim, $str), $max));
foreach ($chunks as $chunk) {
$values = array();
parse_str($chunk, $values);
if (ini_get_bool('magic_quotes_gpc')) {
// Use the same logic as lib/setup.php to work around deprecated magic quotes.
$values = array_map('stripslashes_deep', $values);
}
merge_query_params($_POST, $values);
merge_query_params($_REQUEST, $values);
}
}
/**
* Merge parsed POST chunks.
*
* NOTE: this is not perfect, but it should work in most cases hopefully.
*
* @param array $target
* @param array $values
*/
function merge_query_params(array &$target, array $values) {
if (isset($values[0]) and isset($target[0])) {
// This looks like a split [] array, lets verify the keys are continuous starting with 0.
$keys1 = array_keys($values);
$keys2 = array_keys($target);
if ($keys1 === array_keys($keys1) and $keys2 === array_keys($keys2)) {
foreach ($values as $v) {
$target[] = $v;
}
return;
}
}
foreach ($values as $k => $v) {
if (!isset($target[$k])) {
$target[$k] = $v;
continue;
}
if (is_array($target[$k]) and is_array($v)) {
merge_query_params($target[$k], $v);
continue;
}
// We should not get here unless there are duplicates in params.
$target[$k] = $v;
}
}
/**
* Initializes our performance info early.
*

View File

@ -213,4 +213,82 @@ class core_setuplib_testcase extends advanced_testcase {
$this->assertFileExists($timestampfile);
$this->assertTimeCurrent(filemtime($timestampfile));
}
public function test_merge_query_params() {
$original = array(
'id' => '1',
'course' => '2',
'action' => 'delete',
'grade' => array(
0 => 'a',
1 => 'b',
2 => 'c',
),
'items' => array(
'a' => 'aa',
'b' => 'bb',
),
'mix' => array(
0 => '2',
),
'numerical' => array(
'2' => array('a' => 'b'),
'1' => '2',
),
);
$chunk = array(
'numerical' => array(
'0' => 'z',
'2' => array('d' => 'e'),
),
'action' => 'create',
'next' => '2',
'grade' => array(
0 => 'e',
1 => 'f',
2 => 'g',
),
'mix' => 'mix',
);
$expected = array(
'id' => '1',
'course' => '2',
'action' => 'create',
'grade' => array(
0 => 'a',
1 => 'b',
2 => 'c',
3 => 'e',
4 => 'f',
5 => 'g',
),
'items' => array(
'a' => 'aa',
'b' => 'bb',
),
'mix' => 'mix',
'numerical' => array(
'2' => array('a' => 'b', 'd' => 'e'),
'1' => '2',
'0' => 'z',
),
'next' => '2',
);
$array = $original;
merge_query_params($array, $chunk);
$this->assertSame($expected, $array);
$this->assertNotSame($original, $array);
$query = "id=1&course=2&action=create&grade%5B%5D=a&grade%5B%5D=b&grade%5B%5D=c&grade%5B%5D=e&grade%5B%5D=f&grade%5B%5D=g&items%5Ba%5D=aa&items%5Bb%5D=bb&mix=mix&numerical%5B2%5D%5Ba%5D=b&numerical%5B2%5D%5Bd%5D=e&numerical%5B1%5D=2&numerical%5B0%5D=z&next=2";
$decoded = array();
parse_str($query, $decoded);
$this->assertSame($expected, $decoded);
// Prove that we cannot use array_merge_recursive() instead.
$this->assertNotSame($expected, array_merge_recursive($original, $chunk));
}
}