summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2014-04-06 21:20:00 (GMT)
committerAlex Pott2014-04-06 21:20:00 (GMT)
commit0ac4b9108c59d17a7369ee0b04d18c3d106e0bcd (patch)
tree3a733a5c0dff50dd07477fa264ae3a3514630f00
parent08c18e12036a29a712fb4f97688b980a4216352f (diff)
Issue #1273968 by Sutharsan, Sweetchuck, penyaskito, chx: Remove eval from locale.module.
-rw-r--r--core/lib/Drupal/Component/Gettext/PoHeader.php181
-rw-r--r--core/misc/drupal.js14
-rw-r--r--core/modules/locale/lib/Drupal/locale/Tests/LocalePluralFormatTest.php6
-rw-r--r--core/modules/locale/locale.module19
4 files changed, 191 insertions, 29 deletions
diff --git a/core/lib/Drupal/Component/Gettext/PoHeader.php b/core/lib/Drupal/Component/Gettext/PoHeader.php
index 865231e..5088104 100644
--- a/core/lib/Drupal/Component/Gettext/PoHeader.php
+++ b/core/lib/Drupal/Component/Gettext/PoHeader.php
@@ -189,10 +189,14 @@ class PoHeader {
* 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 @@ class PoHeader {
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 @@ class PoHeader {
}
/**
- * 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 @@ class PoHeader {
* 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 @@ class PoHeader {
$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 @@ class PoHeader {
}
// 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 @@ class PoHeader {
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];
+ }
}
diff --git a/core/misc/drupal.js b/core/misc/drupal.js
index afac700..458d385 100644
--- a/core/misc/drupal.js
+++ b/core/misc/drupal.js
@@ -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];
};
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocalePluralFormatTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocalePluralFormatTest.php
index 195453a..b1982f6 100644
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocalePluralFormatTest.php
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocalePluralFormatTest.php
@@ -98,11 +98,15 @@ class LocalePluralFormatTest extends WebTestBase {
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 @@ class LocalePluralFormatTest extends WebTestBase {
0 => 2,
2 => 1,
8 => 2,
+ 123 => 1,
+ 235 => 2,
),
'hu' => array(
1 => -1,
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index e6d9a6f..59b3be0 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -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