Commit 0ac4b910 authored by alexpott's avatar alexpott

Issue #1273968 by Sutharsan, Sweetchuck, penyaskito, chx: Remove eval from locale.module.

parent 08c18e12
......@@ -189,10 +189,14 @@ public function __toString() {
* The Plural-Forms entry value.
*
* @return
* An array containing the number of plural forms and the converted version
* of the formula that can be evaluated with PHP later.
* An indexed array of parsed plural formula data. Containing:
* - 'nplurals': The number of plural forms defined by the plural formula.
* - 'plurals': Array of plural positions keyed by plural value.
*
* @throws Exception
*/
function parsePluralForms($pluralforms) {
$plurals = array();
// First, delete all whitespace.
$pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
......@@ -214,14 +218,31 @@ function parsePluralForms($pluralforms) {
return FALSE;
}
// Get PHP version of the plural formula.
$plural = $this->parseArithmetic($plural);
// If the number of plurals is zero, we return a default result.
if ($nplurals == 0) {
return array($nplurals, array('default' => 0));
}
// Calculate possible plural positions of different plural values. All known
// plural formula's are repetitive above 100.
// For data compression we store the last position the array value
// changes and store it as default.
$element_stack = $this->parseArithmetic($plural);
$default = 0;
if ($element_stack !== FALSE) {
for ($i = 0; $i <= 199; $i++) {
$plurals[$i] = $this->evaluatePlural($element_stack, $i);
}
$default = $plurals[$i - 1];
$plurals = array_filter($plurals, function ($value) use ($default) {
return ($value != $default);
});
$plurals['default'] = $default;
if ($plural !== FALSE) {
return array($nplurals, $plural);
return array($nplurals, $plurals);
}
else {
throw new Exception('The plural formula could not be parsed.');
throw new \Exception('The plural formula could not be parsed.');
}
}
......@@ -247,7 +268,7 @@ private function parseHeader($header) {
}
/**
* Parses and sanitizes an arithmetic formula into a PHP expression.
* Parses and sanitizes an arithmetic formula into a plural element stack.
*
* While parsing, we ensure, that the operators have the right
* precedence and associativity.
......@@ -256,7 +277,7 @@ private function parseHeader($header) {
* A string containing the arithmetic formula.
*
* @return
* A version of the formula to evaluate with PHP later.
* A stack of values and operations to be evaluated.
*/
private function parseArithmetic($string) {
// Operator precedence table.
......@@ -319,8 +340,9 @@ private function parseArithmetic($string) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
$return = $element_stack;
// Now extract formula from stack.
// Now validate stack.
$previous_size = count($element_stack) + 1;
while (count($element_stack) < $previous_size) {
$previous_size = count($element_stack);
......@@ -343,12 +365,7 @@ private function parseArithmetic($string) {
}
// If only one element is left, the number of operators is appropriate.
if (count($element_stack) == 1) {
return $element_stack[0];
}
else {
return FALSE;
}
return count($element_stack) == 1 ? $return : FALSE;
}
/**
......@@ -416,4 +433,136 @@ private function tokenizeFormula($formula) {
return $tokens;
}
/**
* Evaluate the plural element stack using a plural value.
*
* Using an element stack, which represents a plural formula, we calculate
* which plural string should be used for a given plural value.
*
* An example of plural formula parting and evaluation:
* Plural formula: 'n!=1'
* This formula is parsed by parseArithmetic() to a stack (array) of elements:
* array(
* 0 => '$n',
* 1 => '1',
* 2 => '!=',
* );
* The evaluatePlural() method evaluates the $element_stack using the plural
* value $n. Before the actual evaluation, the '$n' in the array is replaced
* by the value of $n.
* For example: $n = 2 results in:
* array(
* 0 => '2',
* 1 => '1',
* 2 => '!=',
* );
* The stack is processed until only one element is (the result) is left. In
* every iteration the top elements of the stack, up until the first operator,
* are evaluated. After evaluation the arguments and the operator itself are
* removed and replaced by the evaluation result. This is typically 2
* arguments and 1 element for the operator.
* Because the operator is '!=' the example stack is evaluated as:
* $f = (int) 2 != 1;
* The resulting stack is:
* array(
* 0 => 1,
* );
* With only one element left in the stack (the final result) the loop is
* terminated and the result is returned.
*
* @param array $element_stack
* Array of plural formula values and operators create by parseArithmetic().
* @param integer $n
* The @count number for which we are determining the right plural position.
*
* @return integer
* Number of the plural string to be used for the given plural value.
*
* @see parseArithmetic()
* @throws Exception
*/
protected function evaluatePlural($element_stack, $n) {
$count = count($element_stack);
$limit = $count;
// Replace the '$n' value in the formula by the plural value.
for ($i = 0; $i < $count; $i++) {
if ($element_stack[$i] === '$n') {
$element_stack[$i] = $n;
}
}
// We process the stack until only one element is (the result) is left.
// We limit the number of evaluation cycles to prevent an endless loop in
// case the stack contains an error.
while (isset($element_stack[1])) {
for ($i = 2; $i < $count; $i++) {
// There's no point in checking non-symbols. Also, switch(TRUE) would
// match any case and so it would break.
if (is_bool($element_stack[$i]) || is_numeric($element_stack[$i])) {
continue;
}
$f = NULL;
$length = 3;
$delta = 2;
switch ($element_stack[$i]) {
case '==':
$f = $element_stack[$i - 2] == $element_stack[$i - 1];
break;
case '!=':
$f = $element_stack[$i - 2] != $element_stack[$i - 1];
break;
case '<=':
$f = $element_stack[$i - 2] <= $element_stack[$i - 1];
break;
case '>=':
$f = $element_stack[$i - 2] >= $element_stack[$i - 1];
break;
case '<':
$f = $element_stack[$i - 2] < $element_stack[$i - 1];
break;
case '>':
$f = $element_stack[$i - 2] > $element_stack[$i - 1];
break;
case '+':
$f = $element_stack[$i - 2] + $element_stack[$i - 1];
break;
case '-':
$f = $element_stack[$i - 2] - $element_stack[$i - 1];
break;
case '*':
$f = $element_stack[$i - 2] * $element_stack[$i - 1];
break;
case '/':
$f = $element_stack[$i - 2] / $element_stack[$i - 1];
break;
case '%':
$f = $element_stack[$i - 2] % $element_stack[$i - 1];
break;
case '&&':
$f = $element_stack[$i - 2] && $element_stack[$i - 1];
break;
case '||':
$f = $element_stack[$i - 2] || $element_stack[$i - 1];
break;
case ':':
$f = $element_stack[$i - 3] ? $element_stack[$i - 2] : $element_stack[$i - 1];
// This operator has 3 preceding elements, instead of the default 2.
$length = 5;
$delta = 3;
break;
}
// If the element is an operator we remove the processed elements and
// store the result.
if (isset($f)) {
array_splice($element_stack, $i - $delta, $length, $f);
break;
}
}
}
if (!$limit) {
throw new \Exception('The plural formula could not be evaluated.');
}
return (int) $element_stack[0];
}
}
......@@ -364,13 +364,17 @@ if (window.jQuery) {
args = args || {};
args['@count'] = count;
var pluralDelimiter = drupalSettings.locale.pluralDelimiter;
var pluralDelimiter = drupalSettings.locale.pluralDelimiter,
translations = Drupal.t(singular + pluralDelimiter + plural, args, options).split(pluralDelimiter),
index = 0;
// Determine the index of the plural form.
var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] === 1) ? 0 : 1);
var translations = Drupal
.t(singular + pluralDelimiter + plural, args, options)
.split(pluralDelimiter);
if (Drupal.locale.pluralFormula) {
index = count in Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula[count] : Drupal.locale.pluralFormula['default'];
}
else if (args['@count'] !== 1) {
index = 1;
}
return translations[index];
};
......
......@@ -98,11 +98,15 @@ function testGetPluralFormat() {
1 => 0,
0 => 1,
5 => 1,
123 => 1,
235 => 1,
),
'fr' => array(
1 => 0,
0 => 0,
5 => 1,
123 => 1,
235 => 1,
),
'hr' => array(
1 => 0,
......@@ -110,6 +114,8 @@ function testGetPluralFormat() {
0 => 2,
2 => 1,
8 => 2,
123 => 1,
235 => 2,
),
'hu' => array(
1 => -1,
......
......@@ -311,9 +311,11 @@ function locale_get_plural($count, $langcode = NULL) {
// $count and statically cache the result for the combination of language
// and count, since the result will always be identical.
if (!empty($plural_formulas[$langcode])) {
// $n is used inside the expression in the eval().
$n = $count;
$plural_indexes[$langcode][$count] = @eval('return intval(' . $plural_formulas[$langcode]['formula'] . ');');
// Plural formulas are stored as an array for 0-199. 100 is the highest
// modulo used but storing 0-99 is not enough because below 100 we often
// find exceptions (1, 2, etc).
$index = $count > 199 ? 100 + ($count % 100) : $count;
$plural_indexes[$langcode][$count] = isset($plural_formulas[$langcode]['formula'][$index]) ? $plural_formulas[$langcode]['formula'][$index] : $plural_formulas[$langcode]['formula']['default'];
}
// In case there is no plural formula for English (no imported translation
// for English), use a default formula.
......@@ -1272,16 +1274,16 @@ function _locale_rebuild_js($langcode = NULL) {
$data_hash = NULL;
$data = $status = '';
if (!empty($translations)) {
$data = array();
$data = array(
'strings' => $translations,
);
$locale_plurals = \Drupal::state()->get('locale.translation.plurals') ?: array();
if (!empty($locale_plurals[$language->id]['formula'])) {
$data[] = "pluralFormula: function (\$n) { return Number({$locale_plurals[$language->id]['formula']}); }";
$data['pluralFormula'] = $locale_plurals[$language->id]['formula'];
}
$data[] = 'strings: ' . Json::encode($translations);
$data = 'Drupal.locale = { ' . implode(', ', $data) . ' };';
$data = 'Drupal.locale = ' . Json::encode($data) . ';';
$data_hash = Crypt::hashBase64($data);
}
......@@ -1342,6 +1344,7 @@ function _locale_rebuild_js($langcode = NULL) {
case 'updated':
watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => $language->name));
return TRUE;
case 'rebuilt':
watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $locale_javascripts[$language->id]), WATCHDOG_WARNING);
// Proceed to the 'created' case as the JavaScript translation file has
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment