diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 462aea614be..7eec95072fe 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -10572,52 +10572,33 @@ function get_course_display_name_for_list($course) { * Safe analogue of unserialize() that can only parse arrays * * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed. - * Note: If any string (key or value) has semicolon (;) as part of the string parsing will fail. - * This is a simple method to substitute unnecessary unserialize() in code and not intended to cover all possible cases. * * @param string $expression * @return array|bool either parsed array or false if parsing was impossible. */ function unserialize_array($expression) { - $subs = []; - // Find nested arrays, parse them and store in $subs , substitute with special string. - while (preg_match('/([\^;\}])(a:\d+:\{[^\{\}]*\})/', $expression, $matches) && strlen($matches[2]) < strlen($expression)) { - $key = '--SUB' . count($subs) . '--'; - $subs[$key] = unserialize_array($matches[2]); - if ($subs[$key] === false) { - return false; - } - $expression = str_replace($matches[2], $key . ';', $expression); - } // Check the expression is an array. - if (!preg_match('/^a:(\d+):\{([^\}]*)\}$/', $expression, $matches1)) { + if (!preg_match('/^a:(\d+):/', $expression)) { return false; } - // Get the size and elements of an array (key;value;key;value;....). - $parts = explode(';', $matches1[2]); - $size = intval($matches1[1]); - if (count($parts) < $size * 2 + 1) { - return false; - } - // Analyze each part and make sure it is an integer or string or a substitute. - $value = []; - for ($i = 0; $i < $size * 2; $i++) { - if (preg_match('/^i:(\d+)$/', $parts[$i], $matches2)) { - $parts[$i] = (int)$matches2[1]; - } else if (preg_match('/^s:(\d+):"(.*)"$/', $parts[$i], $matches3) && strlen($matches3[2]) == (int)$matches3[1]) { - $parts[$i] = $matches3[2]; - } else if (preg_match('/^--SUB\d+--$/', $parts[$i])) { - $parts[$i] = $subs[$parts[$i]]; - } else { - return false; + + $values = (array) unserialize_object($expression); + + // Callback that returns true if the given value is an unserialized object, executes recursively. + $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool { + if (is_array($value)) { + return (bool) array_filter($value, $invalidvaluecallback); } + return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class); + }; + + // Iterate over the result to ensure there are no stray objects. + if (array_filter($values, $invalidvaluecallback)) { + return false; } - // Combine keys and values. - for ($i = 0; $i < $size * 2; $i += 2) { - $value[$parts[$i]] = $parts[$i+1]; - } - return $value; + + return $values; } /** diff --git a/lib/tests/moodlelib_test.php b/lib/tests/moodlelib_test.php index 1bdc12e732b..ccaa3f6bdcb 100644 --- a/lib/tests/moodlelib_test.php +++ b/lib/tests/moodlelib_test.php @@ -4621,24 +4621,26 @@ EOT; public function test_unserialize_array() { $a = [1, 2, 3]; $this->assertEquals($a, unserialize_array(serialize($a))); - $this->assertEquals($a, unserialize_array(serialize($a))); $a = ['a' => 1, 2 => 2, 'b' => 'cde']; $this->assertEquals($a, unserialize_array(serialize($a))); - $this->assertEquals($a, unserialize_array(serialize($a))); $a = ['a' => 1, 2 => 2, 'b' => 'c"d"e']; $this->assertEquals($a, unserialize_array(serialize($a))); $a = ['a' => 1, 2 => ['c' => 'd', 'e' => 'f'], 'b' => 'cde']; $this->assertEquals($a, unserialize_array(serialize($a))); - - // Can not unserialize if any string contains semicolons. + $a = ['a' => 1, 2 => ['c' => 'd', 'e' => ['f' => 'g']], 'b' => 'cde']; + $this->assertEquals($a, unserialize_array(serialize($a))); $a = ['a' => 1, 2 => 2, 'b' => 'c"d";e']; - $this->assertEquals(false, unserialize_array(serialize($a))); + $this->assertEquals($a, unserialize_array(serialize($a))); // Can not unserialize if there are any objects. $a = (object)['a' => 1, 2 => 2, 'b' => 'cde']; - $this->assertEquals(false, unserialize_array(serialize($a))); + $this->assertFalse(unserialize_array(serialize($a))); $a = ['a' => 1, 2 => 2, 'b' => (object)['a' => 'cde']]; - $this->assertEquals(false, unserialize_array(serialize($a))); + $this->assertFalse(unserialize_array(serialize($a))); + $a = ['a' => 1, 2 => 2, 'b' => ['c' => (object)['a' => 'cde']]]; + $this->assertFalse(unserialize_array(serialize($a))); + $a = ['a' => 1, 2 => 2, 'b' => ['c' => new lang_string('no')]]; + $this->assertFalse(unserialize_array(serialize($a))); // Array used in the grader report. $a = array('aggregatesonly' => [51, 34], 'gradesonly' => [21, 45, 78]);