From 0fe552857468aa12097a74e08f51f5aaa983be07 Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Thu, 1 Aug 2013 15:20:12 -0400 Subject: Upgrade Twig library --- inc/lib/Twig/Extension/Core.php | 993 ++++++++++++++++++++++++++++++---------- 1 file changed, 751 insertions(+), 242 deletions(-) (limited to 'inc/lib/Twig/Extension/Core.php') diff --git a/inc/lib/Twig/Extension/Core.php b/inc/lib/Twig/Extension/Core.php index a4153245..e68687b4 100644 --- a/inc/lib/Twig/Extension/Core.php +++ b/inc/lib/Twig/Extension/Core.php @@ -14,6 +14,83 @@ if (!defined('ENT_SUBSTITUTE')) { */ class Twig_Extension_Core extends Twig_Extension { + protected $dateFormats = array('F j, Y H:i', '%d days'); + protected $numberFormat = array(0, '.', ','); + protected $timezone = null; + + /** + * Sets the default format to be used by the date filter. + * + * @param string $format The default date format string + * @param string $dateIntervalFormat The default date interval format string + */ + public function setDateFormat($format = null, $dateIntervalFormat = null) + { + if (null !== $format) { + $this->dateFormats[0] = $format; + } + + if (null !== $dateIntervalFormat) { + $this->dateFormats[1] = $dateIntervalFormat; + } + } + + /** + * Gets the default format to be used by the date filter. + * + * @return array The default date format string and the default date interval format string + */ + public function getDateFormat() + { + return $this->dateFormats; + } + + /** + * Sets the default timezone to be used by the date filter. + * + * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); + } + + /** + * Gets the default timezone to be used by the date filter. + * + * @return DateTimeZone The default timezone currently in use + */ + public function getTimezone() + { + if (null === $this->timezone) { + $this->timezone = new DateTimeZone(date_default_timezone_get()); + } + + return $this->timezone; + } + + /** + * Sets the default format to be used by the number_format filter. + * + * @param integer $decimal The number of decimal places to use. + * @param string $decimalPoint The character(s) to use for the decimal point. + * @param string $thousandSep The character(s) to use for the thousands separator. + */ + public function setNumberFormat($decimal, $decimalPoint, $thousandSep) + { + $this->numberFormat = array($decimal, $decimalPoint, $thousandSep); + } + + /** + * Get the default format used by the number_format filter. + * + * @return array The arguments for number_format() + */ + public function getNumberFormat() + { + return $this->numberFormat; + } + /** * Returns the token parser instance to add to the existing list. * @@ -34,6 +111,9 @@ class Twig_Extension_Core extends Twig_Extension new Twig_TokenParser_From(), new Twig_TokenParser_Set(), new Twig_TokenParser_Spaceless(), + new Twig_TokenParser_Flush(), + new Twig_TokenParser_Do(), + new Twig_TokenParser_Embed(), ); } @@ -46,40 +126,53 @@ class Twig_Extension_Core extends Twig_Extension { $filters = array( // formatting filters - 'date' => new Twig_Filter_Function('twig_date_format_filter'), - 'format' => new Twig_Filter_Function('sprintf'), - 'replace' => new Twig_Filter_Function('twig_strtr'), + new Twig_SimpleFilter('date', 'twig_date_format_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('date_modify', 'twig_date_modify_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('format', 'sprintf'), + new Twig_SimpleFilter('replace', 'strtr'), + new Twig_SimpleFilter('number_format', 'twig_number_format_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('abs', 'abs'), // encoding - 'url_encode' => new Twig_Filter_Function('twig_urlencode_filter'), - 'json_encode' => new Twig_Filter_Function('twig_jsonencode_filter'), + new Twig_SimpleFilter('url_encode', 'twig_urlencode_filter'), + new Twig_SimpleFilter('json_encode', 'twig_jsonencode_filter'), + new Twig_SimpleFilter('convert_encoding', 'twig_convert_encoding'), // string filters - 'title' => new Twig_Filter_Function('twig_title_string_filter', array('needs_environment' => true)), - 'capitalize' => new Twig_Filter_Function('twig_capitalize_string_filter', array('needs_environment' => true)), - 'upper' => new Twig_Filter_Function('strtoupper'), - 'lower' => new Twig_Filter_Function('strtolower'), - 'striptags' => new Twig_Filter_Function('strip_tags'), + new Twig_SimpleFilter('title', 'twig_title_string_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('capitalize', 'twig_capitalize_string_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('upper', 'strtoupper'), + new Twig_SimpleFilter('lower', 'strtolower'), + new Twig_SimpleFilter('striptags', 'strip_tags'), + new Twig_SimpleFilter('trim', 'trim'), + new Twig_SimpleFilter('nl2br', 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))), // array helpers - 'join' => new Twig_Filter_Function('twig_join_filter'), - 'reverse' => new Twig_Filter_Function('twig_reverse_filter'), - 'length' => new Twig_Filter_Function('twig_length_filter', array('needs_environment' => true)), - 'sort' => new Twig_Filter_Function('twig_sort_filter'), - 'merge' => new Twig_Filter_Function('twig_array_merge'), + new Twig_SimpleFilter('join', 'twig_join_filter'), + new Twig_SimpleFilter('split', 'twig_split_filter'), + new Twig_SimpleFilter('sort', 'twig_sort_filter'), + new Twig_SimpleFilter('merge', 'twig_array_merge'), + new Twig_SimpleFilter('batch', 'twig_array_batch'), + + // string/array filters + new Twig_SimpleFilter('reverse', 'twig_reverse_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('length', 'twig_length_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('slice', 'twig_slice', array('needs_environment' => true)), + new Twig_SimpleFilter('first', 'twig_first', array('needs_environment' => true)), + new Twig_SimpleFilter('last', 'twig_last', array('needs_environment' => true)), // iteration and runtime - 'default' => new Twig_Filter_Function('twig_default_filter'), - 'keys' => new Twig_Filter_Function('twig_get_array_keys_filter'), + new Twig_SimpleFilter('default', '_twig_default_filter', array('node_class' => 'Twig_Node_Expression_Filter_Default')), + new Twig_SimpleFilter('keys', 'twig_get_array_keys_filter'), // escaping - 'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), - 'e' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), + new Twig_SimpleFilter('escape', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), + new Twig_SimpleFilter('e', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), ); if (function_exists('mb_get_info')) { - $filters['upper'] = new Twig_Filter_Function('twig_upper_filter', array('needs_environment' => true)); - $filters['lower'] = new Twig_Filter_Function('twig_lower_filter', array('needs_environment' => true)); + $filters[] = new Twig_SimpleFilter('upper', 'twig_upper_filter', array('needs_environment' => true)); + $filters[] = new Twig_SimpleFilter('lower', 'twig_lower_filter', array('needs_environment' => true)); } return $filters; @@ -93,29 +186,33 @@ class Twig_Extension_Core extends Twig_Extension public function getFunctions() { return array( - 'range' => new Twig_Function_Function('range'), - 'constant' => new Twig_Function_Function('constant'), - 'cycle' => new Twig_Function_Function('twig_cycle'), + new Twig_SimpleFunction('range', 'range'), + new Twig_SimpleFunction('constant', 'twig_constant'), + new Twig_SimpleFunction('cycle', 'twig_cycle'), + new Twig_SimpleFunction('random', 'twig_random', array('needs_environment' => true)), + new Twig_SimpleFunction('date', 'twig_date_converter', array('needs_environment' => true)), + new Twig_SimpleFunction('include', 'twig_include', array('needs_environment' => true, 'needs_context' => true, 'is_safe' => array('all'))), ); } /** - * Returns a list of filters to add to the existing list. + * Returns a list of tests to add to the existing list. * - * @return array An array of filters + * @return array An array of tests */ public function getTests() { return array( - 'even' => new Twig_Test_Function('twig_test_even'), - 'odd' => new Twig_Test_Function('twig_test_odd'), - 'defined' => new Twig_Test_Function('twig_test_defined'), - 'sameas' => new Twig_Test_Function('twig_test_sameas'), - 'none' => new Twig_Test_Function('twig_test_none'), - 'null' => new Twig_Test_Function('twig_test_none'), - 'divisibleby' => new Twig_Test_Function('twig_test_divisibleby'), - 'constant' => new Twig_Test_Function('twig_test_constant'), - 'empty' => new Twig_Test_Function('twig_test_empty'), + new Twig_SimpleTest('even', null, array('node_class' => 'Twig_Node_Expression_Test_Even')), + new Twig_SimpleTest('odd', null, array('node_class' => 'Twig_Node_Expression_Test_Odd')), + new Twig_SimpleTest('defined', null, array('node_class' => 'Twig_Node_Expression_Test_Defined')), + new Twig_SimpleTest('sameas', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas')), + new Twig_SimpleTest('none', null, array('node_class' => 'Twig_Node_Expression_Test_Null')), + new Twig_SimpleTest('null', null, array('node_class' => 'Twig_Node_Expression_Test_Null')), + new Twig_SimpleTest('divisibleby', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby')), + new Twig_SimpleTest('constant', null, array('node_class' => 'Twig_Node_Expression_Test_Constant')), + new Twig_SimpleTest('empty', 'twig_test_empty'), + new Twig_SimpleTest('iterable', 'twig_test_iterable'), ); } @@ -129,15 +226,15 @@ class Twig_Extension_Core extends Twig_Extension return array( array( 'not' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'), - '-' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Neg'), - '+' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Pos'), + '-' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Neg'), + '+' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'), ), array( - 'b-and' => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'b-xor' => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'b-or' => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'or' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'and' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-or' => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-xor' => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-and' => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '==' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '!=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '<' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), @@ -146,6 +243,7 @@ class Twig_Extension_Core extends Twig_Extension '<=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '..' => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '+' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '-' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '~' => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), @@ -155,7 +253,6 @@ class Twig_Extension_Core extends Twig_Extension '%' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'is' => array('precedence' => 100, 'callable' => array($this, 'parseTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), 'is not' => array('precedence' => 100, 'callable' => array($this, 'parseNotTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '..' => array('precedence' => 110, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), '**' => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT), ), ); @@ -169,13 +266,35 @@ class Twig_Extension_Core extends Twig_Extension public function parseTestExpression(Twig_Parser $parser, $node) { $stream = $parser->getStream(); - $name = $stream->expect(Twig_Token::NAME_TYPE); + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); $arguments = null; if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { - $arguments = $parser->getExpressionParser()->parseArguments(); + $arguments = $parser->getExpressionParser()->parseArguments(true); } - return new Twig_Node_Expression_Test($node, $name->getValue(), $arguments, $parser->getCurrentToken()->getLine()); + $class = $this->getTestNodeClass($parser, $name, $node->getLine()); + + return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine()); + } + + protected function getTestNodeClass(Twig_Parser $parser, $name, $line) + { + $env = $parser->getEnvironment(); + $testMap = $env->getTests(); + if (!isset($testMap[$name])) { + $message = sprintf('The test "%s" does not exist', $name); + if ($alternatives = $env->computeAlternatives($name, array_keys($env->getTests()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $line, $parser->getFilename()); + } + + if ($testMap[$name] instanceof Twig_SimpleTest) { + return $testMap[$name]->getNodeClass(); + } + + return $testMap[$name] instanceof Twig_Test_Node ? $testMap[$name]->getClass() : 'Twig_Node_Expression_Test'; } /** @@ -192,18 +311,77 @@ class Twig_Extension_Core extends Twig_Extension /** * Cycles over a value. * - * @param ArrayAccess|array $values An array or an ArrayAccess instance - * @param integer $i The cycle value + * @param ArrayAccess|array $values An array or an ArrayAccess instance + * @param integer $position The cycle position * * @return string The next value in the cycle */ -function twig_cycle($values, $i) +function twig_cycle($values, $position) { if (!is_array($values) && !$values instanceof ArrayAccess) { return $values; } - return $values[$i % count($values)]; + return $values[$position % count($values)]; +} + +/** + * Returns a random value depending on the supplied parameter type: + * - a random item from a Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter + * + * @param Twig_Environment $env A Twig_Environment instance + * @param Traversable|array|integer|string $values The values to pick a random item from + * + * @throws Twig_Error_Runtime When $values is an empty array (does not apply to an empty string which is returned as is). + * + * @return mixed A random value from the given sequence + */ +function twig_random(Twig_Environment $env, $values = null) +{ + if (null === $values) { + return mt_rand(); + } + + if (is_int($values) || is_float($values)) { + return $values < 0 ? mt_rand($values, 0) : mt_rand(0, $values); + } + + if ($values instanceof Traversable) { + $values = iterator_to_array($values); + } elseif (is_string($values)) { + if ('' === $values) { + return ''; + } + if (null !== $charset = $env->getCharset()) { + if ('UTF-8' != $charset) { + $values = twig_convert_encoding($values, 'UTF-8', $charset); + } + + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); + } + } + } else { + return $values[mt_rand(0, strlen($values) - 1)]; + } + } + + if (!is_array($values)) { + return $values; + } + + if (0 === count($values)) { + throw new Twig_Error_Runtime('The random function cannot pick from an empty array.'); + } + + return $values[array_rand($values, 1)]; } /** @@ -213,44 +391,143 @@ function twig_cycle($values, $i) * {{ post.published_at|date("m/d/Y") }} * * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|DateInterval|string $date A date + * @param string $format A format + * @param DateTimeZone|string $timezone A timezone + * + * @return string The formatted date + */ +function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null) +{ + if (null === $format) { + $formats = $env->getExtension('core')->getDateFormat(); + $format = $date instanceof DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof DateInterval) { + return $date->format($format); + } + + return twig_date_converter($env, $date, $timezone)->format($format); +} + +/** + * Returns a new date object modified + * + *
+ *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|string $date A date + * @param string $modifier A modifier string + * + * @return DateTime A new date object + */ +function twig_date_modify_filter(Twig_Environment $env, $date, $modifier) +{ + $date = twig_date_converter($env, $date, false); + $date->modify($modifier); + + return $date; +} + +/** + * Converts an input to a DateTime instance. + * + *
+ *    {% if date(user.created_at) < date('+2days') %}
+ *      {# do something #}
+ *    {% endif %}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance * @param DateTime|string $date A date - * @param string $format A format * @param DateTimeZone|string $timezone A timezone * - * @return string The formatter date + * @return DateTime A DateTime instance */ -function twig_date_format_filter($date, $format = 'F j, Y H:i', $timezone = null) +function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null) { - if (!$date instanceof DateTime) { - if (ctype_digit((string) $date)) { - $date = new DateTime('@'.$date); - $date->setTimezone(new DateTimeZone(date_default_timezone_get())); - } else { - $date = new DateTime($date); - } + // determine the timezone + if (!$timezone) { + $defaultTimezone = $env->getExtension('core')->getTimezone(); + } elseif (!$timezone instanceof DateTimeZone) { + $defaultTimezone = new DateTimeZone($timezone); + } else { + $defaultTimezone = $timezone; } - if (null !== $timezone) { - if (!$timezone instanceof DateTimeZone) { - $timezone = new DateTimeZone($timezone); + if ($date instanceof DateTime) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($defaultTimezone); } - $date->setTimezone($timezone); + return $date; + } + + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = '@'.$date; + } + + $date = new DateTime($date, $defaultTimezone); + if (false !== $timezone) { + $date->setTimezone($defaultTimezone); + } + + return $date; +} + +/** + * Number format filter. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $number A float/int/string of the number to format + * @param integer $decimal The number of decimal points to display. + * @param string $decimalPoint The character(s) to use for the decimal point. + * @param string $thousandSep The character(s) to use for the thousands separator. + * + * @return string The formatted number + */ +function twig_number_format_filter(Twig_Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) +{ + $defaults = $env->getExtension('core')->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } + + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } + + if (null === $thousandSep) { + $thousandSep = $defaults[2]; } - return $date->format($format); + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); } /** - * URL encodes a string. + * URL encodes a string as a path segment or an array as a query string. * - * @param string $url A URL - * @param bool $raw true to use rawurlencode() instead of urlencode + * @param string|array $url A URL or an array of query parameters + * @param bool $raw true to use rawurlencode() instead of urlencode * * @return string The URL encoded value */ function twig_urlencode_filter($url, $raw = false) { + if (is_array($url)) { + return http_build_query($url, '', '&'); + } + if ($raw) { return rawurlencode($url); } @@ -260,7 +537,7 @@ function twig_urlencode_filter($url, $raw = false) if (version_compare(PHP_VERSION, '5.3.0', '<')) { /** - * JSON encodes a PHP variable. + * JSON encodes a variable. * * @param mixed $value The value to encode. * @param integer $options Not used on PHP 5.2.x @@ -279,7 +556,7 @@ if (version_compare(PHP_VERSION, '5.3.0', '<')) { } } else { /** - * JSON encodes a PHP variable. + * JSON encodes a variable. * * @param mixed $value The value to encode. * @param integer $options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT @@ -324,12 +601,72 @@ function _twig_markup2string(&$value) function twig_array_merge($arr1, $arr2) { if (!is_array($arr1) || !is_array($arr2)) { - throw new Twig_Error_Runtime('The merge filter only work with arrays or hashes.'); + throw new Twig_Error_Runtime('The merge filter only works with arrays or hashes.'); } return array_merge($arr1, $arr2); } +/** + * Slices a variable. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * @param integer $start Start of the slice + * @param integer $length Size of the slice + * @param Boolean $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + */ +function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false) +{ + if ($item instanceof Traversable) { + $item = iterator_to_array($item, false); + } + + if (is_array($item)) { + return array_slice($item, $start, $length, $preserveKeys); + } + + $item = (string) $item; + + if (function_exists('mb_get_info') && null !== $charset = $env->getCharset()) { + return mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset); + } + + return null === $length ? substr($item, $start) : substr($item, $start, $length); +} + +/** + * Returns the first element of the item. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * + * @return mixed The first element of the item + */ +function twig_first(Twig_Environment $env, $item) +{ + $elements = twig_slice($env, $item, 0, 1, false); + + return is_string($elements) ? $elements[0] : current($elements); +} + +/** + * Returns the last element of the item. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * + * @return mixed The last element of the item + */ +function twig_last(Twig_Environment $env, $item) +{ + $elements = twig_slice($env, $item, -1, 1, false); + + return is_string($elements) ? $elements[0] : current($elements); +} + /** * Joins the values to a string. * @@ -350,30 +687,55 @@ function twig_array_merge($arr1, $arr2) */ function twig_join_filter($value, $glue = '') { + if ($value instanceof Traversable) { + $value = iterator_to_array($value, false); + } + return implode($glue, (array) $value); } /** - * Returns the value or the default value when it is undefined or empty. + * Splits the string into an array. * *
+ *  {{ "one,two,three"|split(',') }}
+ *  {# returns [one, two, three] #}
+ *
+ *  {{ "one,two,three,four,five"|split(',', 3) }}
+ *  {# returns [one, two, "three,four,five"] #}
  *
- *  {{ var.foo|default('foo item on var is not defined') }}
+ *  {{ "123"|split('') }}
+ *  {# returns [1, 2, 3] #}
  *
+ *  {{ "aabbcc"|split('', 2) }}
+ *  {# returns [aa, bb, cc] #}
  * 
* - * @param mixed $value A value - * @param mixed $default The default value + * @param string $value A string + * @param string $delimiter The delimiter + * @param integer $limit The limit * - * @param mixed The value or the default value; + * @return array The split string as an array */ -function twig_default_filter($value, $default = '') +function twig_split_filter($value, $delimiter, $limit = null) +{ + if (empty($delimiter)) { + return str_split($value, null === $limit ? 1 : $limit); + } + + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); +} + +// The '_default' filter is used internally to avoid using the ternary operator +// which costs a lot for big contexts (before PHP 5.4). So, on average, +// a function call is cheaper. +function _twig_default_filter($value, $default = '') { if (twig_test_empty($value)) { return $default; - } else { - return $value; } + + return $value; } /** @@ -405,23 +767,43 @@ function twig_get_array_keys_filter($array) } /** - * Reverses an array. + * Reverses a variable. * - * @param array|Traversable $array An array or a Traversable instance + * @param Twig_Environment $env A Twig_Environment instance + * @param array|Traversable|string $item An array, a Traversable instance, or a string + * @param Boolean $preserveKeys Whether to preserve key or not * - * return array The array reversed + * @return mixed The reversed input */ -function twig_reverse_filter($array) +function twig_reverse_filter(Twig_Environment $env, $item, $preserveKeys = false) { - if (is_object($array) && $array instanceof Traversable) { - return array_reverse(iterator_to_array($array)); + if (is_object($item) && $item instanceof Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); } - if (!is_array($array)) { - return array(); + if (is_array($item)) { + return array_reverse($item, $preserveKeys); + } + + if (null !== $charset = $env->getCharset()) { + $string = (string) $item; + + if ('UTF-8' != $charset) { + $item = twig_convert_encoding($string, 'UTF-8', $charset); + } + + preg_match_all('/./us', $item, $matches); + + $string = implode('', array_reverse($matches[0])); + + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; } - return array_reverse($array); + return strrev((string) $item); } /** @@ -440,78 +822,151 @@ function twig_sort_filter($array) function twig_in_filter($value, $compare) { if (is_array($compare)) { - return in_array($value, $compare); + return in_array($value, $compare, is_object($value)); } elseif (is_string($compare)) { + if (!strlen($value)) { + return empty($compare); + } + return false !== strpos($compare, (string) $value); - } elseif (is_object($compare) && $compare instanceof Traversable) { - return in_array($value, iterator_to_array($compare, false)); + } elseif ($compare instanceof Traversable) { + return in_array($value, iterator_to_array($compare, false), is_object($value)); } return false; } -/** - * Replaces placeholders in a string. - * - *
- *  {{ "I like %this% and %that%."|replace({'%this%': foo, '%that%': "bar"}) }}
- * 
- * - * @param string $pattern A string - * @param string $replacements The values for the placeholders - * - * @return string The string where the placeholders have been replaced - */ -function twig_strtr($pattern, $replacements) -{ - return str_replace(array_keys($replacements), array_values($replacements), $pattern); -} - /** * Escapes a string. * - * @param Twig_Environment $env A Twig_Environment instance - * @param string $string The value to be escaped - * @param string $type The escaping strategy - * @param string $charset The charset + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string $charset The charset + * @param Boolean $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) */ -function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $charset = null) +function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) { - if (is_object($string) && $string instanceof Twig_Markup) { + if ($autoescape && $string instanceof Twig_Markup) { return $string; } - if (!is_string($string) && !(is_object($string) && method_exists($string, '__toString'))) { - return $string; + if (!is_string($string)) { + if (is_object($string) && method_exists($string, '__toString')) { + $string = (string) $string; + } else { + return $string; + } } if (null === $charset) { $charset = $env->getCharset(); } - switch ($type) { + switch ($strategy) { + case 'html': + // see http://php.net/htmlspecialchars + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = array( + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ); + + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } + + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; + + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } + + $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + return twig_convert_encoding($string, $charset, 'UTF-8'); + case 'js': // escape all non-alphanumeric characters // into their \xHH or \uHHHH representations if ('UTF-8' != $charset) { - $string = _twig_convert_encoding($string, 'UTF-8', $charset); + $string = twig_convert_encoding($string, 'UTF-8', $charset); } - if (null === $string = preg_replace_callback('#[^\p{L}\p{N} ]#u', '_twig_escape_js_callback', $string)) { + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); } + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string); + if ('UTF-8' != $charset) { - $string = _twig_convert_encoding($string, $charset, 'UTF-8'); + $string = twig_convert_encoding($string, $charset, 'UTF-8'); } return $string; - case 'html': - return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + case 'css': + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } + + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { + throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string); + + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; + + case 'html_attr': + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } + + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { + throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string); + + if ('UTF-8' != $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; + + case 'url': + // hackish test to avoid version_compare that is much slower, this works unless PHP releases a 5.10.* + // at that point however PHP 5.2.* support can be removed + if (PHP_VERSION < '5.3.0') { + return str_replace('%7E', '~', rawurlencode($string)); + } + + return rawurlencode($string); default: - throw new Twig_Error_Runtime(sprintf('Invalid escape type "%s".', $type)); + throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: html, js, url, css, and html_attr).', $strategy)); } } @@ -521,28 +976,26 @@ function twig_escape_filter_is_safe(Twig_Node $filterArgs) foreach ($filterArgs as $arg) { if ($arg instanceof Twig_Node_Expression_Constant) { return array($arg->getAttribute('value')); - } else { - return array(); } - break; + return array(); } return array('html'); } -if (function_exists('iconv')) { - function _twig_convert_encoding($string, $to, $from) +if (function_exists('mb_convert_encoding')) { + function twig_convert_encoding($string, $to, $from) { - return iconv($from, $to, $string); + return mb_convert_encoding($string, $to, $from); } -} elseif (function_exists('mb_convert_encoding')) { - function _twig_convert_encoding($string, $to, $from) +} elseif (function_exists('iconv')) { + function twig_convert_encoding($string, $to, $from) { - return mb_convert_encoding($string, $to, $from); + return iconv($from, $to, $string); } } else { - function _twig_convert_encoding($string, $to, $from) + function twig_convert_encoding($string, $to, $from) { throw new Twig_Error_Runtime('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).'); } @@ -554,22 +1007,98 @@ function _twig_escape_js_callback($matches) // \xHH if (!isset($char[1])) { - return '\\x'.substr('00'.bin2hex($char), -2); + return '\\x'.strtoupper(substr('00'.bin2hex($char), -2)); + } + + // \uHHHH + $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); + + return '\\u'.strtoupper(substr('0000'.bin2hex($char), -4)); +} + +function _twig_escape_css_callback($matches) +{ + $char = $matches[0]; + + // \xHH + if (!isset($char[1])) { + $hex = ltrim(strtoupper(bin2hex($char)), '0'); + if (0 === strlen($hex)) { + $hex = '0'; + } + + return '\\'.$hex.' '; } // \uHHHH - $char = _twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); + $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); - return '\\u'.substr('0000'.bin2hex($char), -4); + return '\\'.ltrim(strtoupper(bin2hex($char)), '0').' '; +} + +/** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +function _twig_escape_html_attr_callback($matches) +{ + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = array( + 34 => 'quot', /* quotation mark */ + 38 => 'amp', /* ampersand */ + 60 => 'lt', /* less-than sign */ + 62 => 'gt', /* greater-than sign */ + ); + + $chr = $matches[0]; + $ord = ord($chr); + + /** + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) { + return '�'; + } + + /** + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (strlen($chr) == 1) { + $hex = strtoupper(substr('00'.bin2hex($chr), -2)); + } else { + $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8'); + $hex = strtoupper(substr('0000'.bin2hex($chr), -4)); + } + + $int = hexdec($hex); + if (array_key_exists($int, $entityMap)) { + return sprintf('&%s;', $entityMap[$int]); + } + + /** + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + + return sprintf('&#x%s;', $hex); } // add multibyte extensions if possible if (function_exists('mb_get_info')) { /** - * Returns the length of a PHP variable. + * Returns the length of a variable. * * @param Twig_Environment $env A Twig_Environment instance - * @param mixed $thing A PHP variable + * @param mixed $thing A variable * * @return integer The length of the value */ @@ -648,13 +1177,12 @@ if (function_exists('mb_get_info')) { } } // and byte fallback -else -{ +else { /** - * Returns the length of a PHP variable. + * Returns the length of a variable. * * @param Twig_Environment $env A Twig_Environment instance - * @param mixed $thing A PHP variable + * @param mixed $thing A variable * * @return integer The length of the value */ @@ -693,154 +1221,135 @@ else /* used internally */ function twig_ensure_traversable($seq) { - if (is_array($seq) || (is_object($seq) && $seq instanceof Traversable)) { + if ($seq instanceof Traversable || is_array($seq)) { return $seq; - } else { - return array(); } + + return array(); } /** - * Checks that a variable points to the same memory address than another one. + * Checks if a variable is empty. * *
- * {% if foo.attribute is sameas(false) %}
- *    the foo attribute really is the ``false`` PHP value
+ * {# evaluates to true if the foo variable is null, false, or the empty string #}
+ * {% if foo is empty %}
+ *     {# ... #}
  * {% endif %}
  * 
* - * @param mixed $value A PHP variable - * @param mixed $test The PHP variable to test against + * @param mixed $value A variable * - * @return Boolean true if the values are the same, false otherwise + * @return Boolean true if the value is empty, false otherwise */ -function twig_test_sameas($value, $test) +function twig_test_empty($value) { - return $value === $test; -} + if ($value instanceof Countable) { + return 0 == count($value); + } -/** - * Checks that a variable is null. - * - *
- *  {{ var is none }}
- * 
- * - * @param mixed $value a PHP variable. - * - * @return Boolean true if the value is null, false otherwise - */ -function twig_test_none($value) -{ - return null === $value; + return '' === $value || false === $value || null === $value || array() === $value; } /** - * Checks if a variable is divisible by a number. + * Checks if a variable is traversable. * *
- *  {% if loop.index is divisibleby(3) %}
+ * {# evaluates to true if the foo variable is an array or a traversable object #}
+ * {% if foo is traversable %}
+ *     {# ... #}
+ * {% endif %}
  * 
* - * @param integer $value A PHP value - * @param integer $num A number + * @param mixed $value A variable * - * @return Boolean true if the value is divisible by the number, false otherwise + * @return Boolean true if the value is traversable */ -function twig_test_divisibleby($value, $num) +function twig_test_iterable($value) { - return 0 == $value % $num; + return $value instanceof Traversable || is_array($value); } /** - * Checks if a number is even. - * - *
- *  {{ var is even }}
- * 
+ * Renders a template. * - * @param integer $value An integer + * @param string $template The template to render + * @param array $variables The variables to pass to the template + * @param Boolean $with_context Whether to pass the current context variables or not + * @param Boolean $ignore_missing Whether to ignore missing templates or not + * @param Boolean $sandboxed Whether to sandbox the template or not * - * @return Boolean true if the value is even, false otherwise + * @return string The rendered template */ -function twig_test_even($value) +function twig_include(Twig_Environment $env, $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false) { - return $value % 2 == 0; -} + if ($withContext) { + $variables = array_merge($context, $variables); + } -/** - * Checks if a number is odd. - * - *
- *  {{ var is odd }}
- * 
- * - * @param integer $value An integer - * - * @return Boolean true if the value is odd, false otherwise - */ -function twig_test_odd($value) -{ - return $value % 2 == 1; -} + if ($isSandboxed = $sandboxed && $env->hasExtension('sandbox')) { + $sandbox = $env->getExtension('sandbox'); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } + } -/** - * Checks if a variable is the exact same value as a constant. - * - *
- *  {% if post.status is constant('Post::PUBLISHED') %}
- *    the status attribute is exactly the same as Post::PUBLISHED
- *  {% endif %}
- * 
- * - * @param mixed $value A PHP value - * @param mixed $constant The constant to test against - * - * @return Boolean true if the value is the same as the constant, false otherwise - */ -function twig_test_constant($value, $constant) -{ - return constant($constant) === $value; + try { + return $env->resolveTemplate($template)->render($variables); + } catch (Twig_Error_Loader $e) { + if (!$ignoreMissing) { + throw $e; + } + } + + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } } /** - * Checks if a variable is defined in the current context. - * - *
- * {# defined works with variable names #}
- * {% if foo is defined %}
- *     {# ... #}
- * {% endif %}
- * 
+ * Provides the ability to get constants from instances as well as class/global constants. * - * @param mixed $name A PHP variable - * @param array $context The current context + * @param string $constant The name of the constant + * @param null|object $object The object to get the constant from * - * @return Boolean true if the value is defined, false otherwise + * @return string */ -function twig_test_defined($name, $context) +function twig_constant($constant, $object = null) { - return array_key_exists($name, $context); + if (null !== $object) { + $constant = get_class($object).'::'.$constant; + } + + return constant($constant); } /** - * Checks if a variable is empty. - * - *
- * {# evaluates to true if the foo variable is null, false, or the empty string #}
- * {% if foo is empty %}
- *     {# ... #}
- * {% endif %}
- * 
+ * Batches item. * - * @param mixed $value A PHP variable + * @param array $items An array of items + * @param integer $size The size of the batch + * @param string $fill A string to fill missing items * - * @return Boolean true if the value is empty, false otherwise + * @return array */ -function twig_test_empty($value) +function twig_array_batch($items, $size, $fill = null) { - if ($value instanceof Countable) { - return 0 == count($value); + if ($items instanceof Traversable) { + $items = iterator_to_array($items, false); } - return false === $value || (empty($value) && '0' != $value); + + $size = ceil($size); + + $result = array_chunk($items, $size, true); + + if (null !== $fill) { + $last = count($result) - 1; + $result[$last] = array_merge( + $result[$last], + array_fill(0, $size - count($result[$last]), $fill) + ); + } + + return $result; } -- cgit v1.3