Rewrote all default modifiers and functions from the plugins folder to PSR-4 classes

This commit is contained in:
Simon Wisselink
2022-12-24 23:38:13 +01:00
parent 57cff64869
commit ab5705a90d
63 changed files with 3318 additions and 3676 deletions

View File

@@ -0,0 +1,27 @@
<?php
namespace Smarty\Extension;
use Smarty\Compile\Tag\BCPluginWrapper;
class BCPluginsAdapter extends Base {
/**
* @var \Smarty\Smarty
*/
private $smarty;
public function __construct(\Smarty\Smarty $smarty) {
$this->smarty = $smarty;
}
//$smarty->registered_plugins[$type][$name] = [$callback, (bool)$cacheable, (array)$cache_attr];
public function getTagCompiler(string $tag): ?\Smarty\Compile\Tag\TagCompilerInterface {
if (!isset($smarty->registered_plugins['compiler'][$tag])) {
return null;
}
$callback = reset($smarty->registered_plugins['compiler'][$tag]);
return new BCPluginWrapper($callback);
}
}

View File

@@ -2,13 +2,23 @@
namespace Smarty\Extension;
use Smarty\FunctionHandler\FunctionHandlerInterface;
class Base implements ExtensionInterface {
public function getTagCompiler(string $tag): ?\Smarty\Compile\Tag\Base {
public function getTagCompiler(string $tag): ?\Smarty\Compile\Tag\TagCompilerInterface {
return null;
}
public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier\Base {
public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier\ModifierCompilerInterface {
return null;
}
public function getFunctionHandler(string $functionName): ?\Smarty\FunctionHandler\FunctionHandlerInterface {
return null;
}
public function getModifierCallback(string $modifierName) {
return null;
}
}

View File

@@ -3,7 +3,7 @@
namespace Smarty\Extension;
class Core extends Base {
public function getTagCompiler(string $tag): ?\Smarty\Compile\Tag\Base {
public function getTagCompiler(string $tag): ?\Smarty\Compile\Tag\TagCompilerInterface {
switch ($tag) {
case 'append': return new \Smarty\Compile\Tag\Append();
case 'assign': return new \Smarty\Compile\Tag\Assign();
@@ -60,7 +60,7 @@ class Core extends Base {
return null;
}
public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier\Base {
public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier\ModifierCompilerInterface {
switch ($modifier) {
case 'cat': return new \Smarty\Compile\Modifier\CatModifierCompiler();
case 'count_characters': return new \Smarty\Compile\Modifier\CountCharactersModifierCompiler();
@@ -88,4 +88,654 @@ class Core extends Base {
return null;
}
public function getModifierCallback(string $modifierName) {
switch ($modifierName) {
case 'spacify': return [$this, 'smarty_modifier_spacify'];
case 'capitalize': return [$this, 'smarty_modifier_capitalize'];
case 'count': return [$this, 'smarty_modifier_count'];
case 'date_format': return [$this, 'smarty_modifier_date_format'];
case 'debug_print_var': return [$this, 'smarty_modifier_debug_print_var'];
case 'escape': return [$this, 'smarty_modifier_escape'];
case 'explode': return [$this, 'smarty_modifier_explode'];
case 'mb_wordwrap': return [$this, 'smarty_modifier_mb_wordwrap'];
case 'number_format': return [$this, 'smarty_modifier_number_format'];
case 'regex_replace': return [$this, 'smarty_modifier_regex_replace'];
case 'replace': return [$this, 'smarty_modifier_replace'];
case 'truncate': return [$this, 'smarty_modifier_truncate'];
}
return null;
}
public function getFunctionHandler(string $functionName): ?\Smarty\FunctionHandler\FunctionHandlerInterface {
switch ($functionName) {
case 'counter': return new \Smarty\FunctionHandler\Counter();
case 'cycle': return new \Smarty\FunctionHandler\Cycle();
case 'fetch': return new \Smarty\FunctionHandler\Fetch();
case 'html_checkboxes': return new \Smarty\FunctionHandler\HtmlCheckboxes();
case 'html_image': return new \Smarty\FunctionHandler\HtmlImage();
case 'html_options': return new \Smarty\FunctionHandler\HtmlOptions();
case 'html_radios': return new \Smarty\FunctionHandler\HtmlRadios();
case 'html_select_date': return new \Smarty\FunctionHandler\HtmlSelectDate();
case 'html_select_time': return new \Smarty\FunctionHandler\HtmlSelectTime();
case 'html_table': return new \Smarty\FunctionHandler\HtmlTable();
case 'mailto': return new \Smarty\FunctionHandler\Mailto();
case 'math': return new \Smarty\FunctionHandler\Math();
}
return null;
}
/**
* Smarty spacify modifier plugin
* Type: modifier
* Name: spacify
* Purpose: add spaces between characters in a string
*
* @link https://www.smarty.net/manual/en/language.modifier.spacify.php spacify (Smarty online manual)
* @author Monte Ohrt <monte at ohrt dot com>
*
* @param string $string input string
* @param string $spacify_char string to insert between characters.
*
* @return string
*/
private function smarty_modifier_spacify($string, $spacify_char = ' ')
{
// well… what about charsets besides latin and UTF-8?
return implode($spacify_char, preg_split('//' . \Smarty\Smarty::$_UTF8_MODIFIER, $string, -1, PREG_SPLIT_NO_EMPTY));
}
/**
* Smarty capitalize modifier plugin
* Type: modifier
* Name: capitalize
* Purpose: capitalize words in the string
* {@internal {$string|capitalize:true:true} is the fastest option for MBString enabled systems }}
*
* @param string $string string to capitalize
* @param boolean $uc_digits also capitalize "x123" to "X123"
* @param boolean $lc_rest capitalize first letters, lowercase all following letters "aAa" to "Aaa"
*
* @return string capitalized string
* @author Monte Ohrt <monte at ohrt dot com>
* @author Rodney Rehm
*/
private function smarty_modifier_capitalize($string, $uc_digits = false, $lc_rest = false)
{
$string = (string) $string;
if ($lc_rest) {
// uppercase (including hyphenated words)
$upper_string = mb_convert_case($string, MB_CASE_TITLE, \Smarty\Smarty::$_CHARSET);
} else {
// uppercase word breaks
$upper_string = preg_replace_callback(
"!(^|[^\p{L}'])([\p{Ll}])!S" . \Smarty\Smarty::$_UTF8_MODIFIER,
function ($matches) {
return stripslashes($matches[1]) .
mb_convert_case(stripslashes($matches[2]), MB_CASE_UPPER, \Smarty\Smarty::$_CHARSET);
},
$string
);
}
// check uc_digits case
if (!$uc_digits) {
if (preg_match_all(
"!\b([\p{L}]*[\p{N}]+[\p{L}]*)\b!" . \Smarty\Smarty::$_UTF8_MODIFIER,
$string,
$matches,
PREG_OFFSET_CAPTURE
)
) {
foreach ($matches[ 1 ] as $match) {
$upper_string =
substr_replace(
$upper_string,
mb_strtolower($match[ 0 ], \Smarty\Smarty::$_CHARSET),
$match[ 1 ],
strlen($match[ 0 ])
);
}
}
}
$upper_string =
preg_replace_callback(
"!((^|\s)['\"])(\w)!" . \Smarty\Smarty::$_UTF8_MODIFIER,
function ($matches) {
return stripslashes(
$matches[ 1 ]) . mb_convert_case(stripslashes($matches[ 3 ]),
MB_CASE_UPPER,
\Smarty\Smarty::$_CHARSET
);
},
$upper_string
);
return $upper_string;
}
/**
* Smarty count modifier plugin
* Type: modifier
* Name: count
* Purpose: counts all elements in an array or in a Countable object
* Input:
* - Countable|array: array or object to count
* - mode: int defaults to 0 for normal count mode, if set to 1 counts recursive
*
* @param mixed $arrayOrObject input array/object
* @param int $mode count mode
*
* @return int
*/
private function smarty_modifier_count($arrayOrObject, $mode = 0) {
/*
* @see https://www.php.net/count
* > Prior to PHP 8.0.0, if the parameter was neither an array nor an object that implements the Countable interface,
* > 1 would be returned, unless value was null, in which case 0 would be returned.
*/
if ($arrayOrObject instanceof Countable || is_array($arrayOrObject)) {
return count($arrayOrObject, (int) $mode);
} elseif ($arrayOrObject === null) {
return 0;
}
return 1;
}
/**
* Smarty date_format modifier plugin
* Type: modifier
* Name: date_format
* Purpose: format datestamps via strftime
* Input:
* - string: input date string
* - format: strftime format for output
* - default_date: default date if $string is empty
*
* @link https://www.smarty.net/manual/en/language.modifier.date.format.php date_format (Smarty online manual)
* @author Monte Ohrt <monte at ohrt dot com>
*
* @param string $string input date string
* @param string $format strftime format for output
* @param string $default_date default date if $string is empty
* @param string $formatter either 'strftime' or 'auto'
*
* @return string |void
* @uses smarty_make_timestamp()
*/
private function smarty_modifier_date_format($string, $format = null, $default_date = '', $formatter = 'auto')
{
if ($format === null) {
$format = \Smarty\Smarty::$_DATE_FORMAT;
}
if (!empty($string) && $string !== '0000-00-00' && $string !== '0000-00-00 00:00:00') {
$timestamp = smarty_make_timestamp($string);
} elseif (!empty($default_date)) {
$timestamp = smarty_make_timestamp($default_date);
} else {
return;
}
if ($formatter === 'strftime' || ($formatter === 'auto' && strpos($format, '%') !== false)) {
if (\Smarty\Smarty::$_IS_WINDOWS) {
$_win_from = array(
'%D',
'%h',
'%n',
'%r',
'%R',
'%t',
'%T'
);
$_win_to = array(
'%m/%d/%y',
'%b',
"\n",
'%I:%M:%S %p',
'%H:%M',
"\t",
'%H:%M:%S'
);
if (strpos($format, '%e') !== false) {
$_win_from[] = '%e';
$_win_to[] = sprintf('%\' 2d', date('j', $timestamp));
}
if (strpos($format, '%l') !== false) {
$_win_from[] = '%l';
$_win_to[] = sprintf('%\' 2d', date('h', $timestamp));
}
$format = str_replace($_win_from, $_win_to, $format);
}
// @ to suppress deprecation errors when running in PHP8.1 or higher.
return @strftime($format, $timestamp);
} else {
return date($format, $timestamp);
}
}
/**
* Smarty debug_print_var modifier plugin
* Type: modifier
* Name: debug_print_var
* Purpose: formats variable contents for display in the console
*
* @author Monte Ohrt <monte at ohrt dot com>
*
* @param array|object $var variable to be formatted
* @param int $max maximum recursion depth if $var is an array or object
* @param int $length maximum string length if $var is a string
* @param int $depth actual recursion depth
* @param array $objects processed objects in actual depth to prevent recursive object processing
*
* @return string
*/
private function smarty_modifier_debug_print_var($var, $max = 10, $length = 40, $depth = 0, $objects = array())
{
$_replace = array("\n" => '\n', "\r" => '\r', "\t" => '\t');
switch (gettype($var)) {
case 'array':
$results = '<b>Array (' . count($var) . ')</b>';
if ($depth === $max) {
break;
}
foreach ($var as $curr_key => $curr_val) {
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b>' . strtr($curr_key, $_replace) .
'</b> =&gt; ' .
$this->smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
$depth--;
}
break;
case 'object':
$object_vars = get_object_vars($var);
$results = '<b>' . get_class($var) . ' Object (' . count($object_vars) . ')</b>';
if (in_array($var, $objects)) {
$results .= ' called recursive';
break;
}
if ($depth === $max) {
break;
}
$objects[] = $var;
foreach ($object_vars as $curr_key => $curr_val) {
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b> -&gt;' . strtr($curr_key, $_replace) .
'</b> = ' . $this->smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
$depth--;
}
break;
case 'boolean':
case 'NULL':
case 'resource':
if (true === $var) {
$results = 'true';
} elseif (false === $var) {
$results = 'false';
} elseif (null === $var) {
$results = 'null';
} else {
$results = htmlspecialchars((string)$var);
}
$results = '<i>' . $results . '</i>';
break;
case 'integer':
case 'float':
$results = htmlspecialchars((string)$var);
break;
case 'string':
$results = strtr($var, $_replace);
if (mb_strlen($var, \Smarty\Smarty::$_CHARSET) > $length) {
$results = mb_substr($var, 0, $length - 3, \Smarty\Smarty::$_CHARSET) . '...';
}
$results = htmlspecialchars('"' . $results . '"', ENT_QUOTES, \Smarty\Smarty::$_CHARSET);
break;
case 'unknown type':
default:
$results = strtr((string)$var, $_replace);
if (mb_strlen($results, \Smarty\Smarty::$_CHARSET) > $length) {
$results = mb_substr($results, 0, $length - 3, \Smarty\Smarty::$_CHARSET) . '...';
}
$results = htmlspecialchars($results, ENT_QUOTES, \Smarty\Smarty::$_CHARSET);
}
return $results;
}
/**
* Smarty escape modifier plugin
* Type: modifier
* Name: escape
* Purpose: escape string for output
*
* @link https://www.smarty.net/docs/en/language.modifier.escape
* @author Monte Ohrt <monte at ohrt dot com>
*
* @param string $string input string
* @param string $esc_type escape type
* @param string $char_set character set, used for htmlspecialchars() or htmlentities()
* @param boolean $double_encode encode already encoded entitites again, used for htmlspecialchars() or htmlentities()
*
* @return string escaped input string
*/
private function smarty_modifier_escape($string, $esc_type = 'html', $char_set = null, $double_encode = true)
{
if (!$char_set) {
$char_set = \Smarty\Smarty::$_CHARSET;
}
$string = (string)$string;
switch ($esc_type) {
case 'html':
return htmlspecialchars($string, ENT_QUOTES, $char_set, $double_encode);
// no break
case 'htmlall':
$string = mb_convert_encoding($string, 'UTF-8', $char_set);
return htmlentities($string, ENT_QUOTES, 'UTF-8', $double_encode);
// no break
case 'url':
return rawurlencode($string);
case 'urlpathinfo':
return str_replace('%2F', '/', rawurlencode($string));
case 'quotes':
// escape unescaped single quotes
return preg_replace("%(?<!\\\\)'%", "\\'", $string);
case 'hex':
// escape every byte into hex
// Note that the UTF-8 encoded character ä will be represented as %c3%a4
$return = '';
$_length = strlen($string);
for ($x = 0; $x < $_length; $x++) {
$return .= '%' . bin2hex($string[ $x ]);
}
return $return;
case 'hexentity':
$return = '';
foreach ($this->mb_to_unicode($string, \Smarty\Smarty::$_CHARSET) as $unicode) {
$return .= '&#x' . strtoupper(dechex($unicode)) . ';';
}
return $return;
case 'decentity':
$return = '';
foreach ($this->mb_to_unicode($string, \Smarty\Smarty::$_CHARSET) as $unicode) {
$return .= '&#' . $unicode . ';';
}
return $return;
case 'javascript':
// escape quotes and backslashes, newlines, etc.
return strtr(
$string,
array(
'\\' => '\\\\',
"'" => "\\'",
'"' => '\\"',
"\r" => '\\r',
"\n" => '\\n',
'</' => '<\/',
// see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
'<!--' => '<\!--',
'<s' => '<\s',
'<S' => '<\S'
)
);
case 'mail':
return smarty_mb_str_replace(
array(
'@',
'.'
),
array(
' [AT] ',
' [DOT] '
),
$string
);
case 'nonstd':
// escape non-standard chars, such as ms document quotes
$return = '';
foreach ($this->mb_to_unicode($string, \Smarty\Smarty::$_CHARSET) as $unicode) {
if ($unicode >= 126) {
$return .= '&#' . $unicode . ';';
} else {
$return .= chr($unicode);
}
}
return $return;
default:
trigger_error("escape: unsupported type: $esc_type - returning unmodified string", E_USER_NOTICE);
return $string;
}
}
/**
* convert characters to their decimal unicode equivalents
*
* @link http://www.ibm.com/developerworks/library/os-php-unicode/index.html#listing3 for inspiration
*
* @param string $string characters to calculate unicode of
* @param string $encoding encoding of $string, if null mb_internal_encoding() is used
*
* @return array sequence of unicodes
* @author Rodney Rehm
*/
private function mb_to_unicode($string, $encoding = null) {
if ($encoding) {
$expanded = mb_convert_encoding($string, 'UTF-32BE', $encoding);
} else {
$expanded = mb_convert_encoding($string, 'UTF-32BE');
}
return unpack('N*', $expanded);
}
/**
* Smarty explode modifier plugin
* Type: modifier
* Name: explode
* Purpose: split a string by a string
*
* @param string $separator
* @param string $string
* @param int|null $limit
*
* @return array
*/
private function smarty_modifier_explode($separator, $string, ?int $limit = null)
{
// provide $string default to prevent deprecation errors in PHP >=8.1
return explode($separator, $string ?? '', $limit ?? PHP_INT_MAX);
}
/**
* Smarty wordwrap modifier plugin
* Type: modifier
* Name: mb_wordwrap
* Purpose: Wrap a string to a given number of characters
*
* @link https://php.net/manual/en/function.wordwrap.php for similarity
*
* @param string $str the string to wrap
* @param int $width the width of the output
* @param string $break the character used to break the line
* @param boolean $cut ignored parameter, just for the sake of
*
* @return string wrapped string
* @author Rodney Rehm
*/
private function smarty_modifier_mb_wordwrap($str, $width = 75, $break = "\n", $cut = false)
{
// break words into tokens using white space as a delimiter
$tokens = preg_split('!(\s)!S' . \Smarty\Smarty::$_UTF8_MODIFIER, $str, -1, PREG_SPLIT_NO_EMPTY + PREG_SPLIT_DELIM_CAPTURE);
$length = 0;
$t = '';
$_previous = false;
$_space = false;
foreach ($tokens as $_token) {
$token_length = mb_strlen($_token, \Smarty\Smarty::$_CHARSET);
$_tokens = array($_token);
if ($token_length > $width) {
if ($cut) {
$_tokens = preg_split(
'!(.{' . $width . '})!S' . \Smarty\Smarty::$_UTF8_MODIFIER,
$_token,
-1,
PREG_SPLIT_NO_EMPTY + PREG_SPLIT_DELIM_CAPTURE
);
}
}
foreach ($_tokens as $token) {
$_space = !!preg_match('!^\s$!S' . \Smarty\Smarty::$_UTF8_MODIFIER, $token);
$token_length = mb_strlen($token, \Smarty\Smarty::$_CHARSET);
$length += $token_length;
if ($length > $width) {
// remove space before inserted break
if ($_previous) {
$t = mb_substr($t, 0, -1, \Smarty\Smarty::$_CHARSET);
}
if (!$_space) {
// add the break before the token
if (!empty($t)) {
$t .= $break;
}
$length = $token_length;
}
} elseif ($token === "\n") {
// hard break must reset counters
$length = 0;
}
$_previous = $_space;
// add the token
$t .= $token;
}
}
return $t;
}
/**
* Smarty number_format modifier plugin
* Type: modifier
* Name: number_format
* Purpose: Format a number with grouped thousands
*
* @param float|null $num
* @param int $decimals
* @param string|null $decimal_separator
* @param string|null $thousands_separator
*
* @return string
*/
private function smarty_modifier_number_format(?float $num, int $decimals = 0, ?string $decimal_separator = ".", ?string $thousands_separator = ",")
{
// provide $num default to prevent deprecation errors in PHP >=8.1
return number_format($num ?? 0.0, $decimals, $decimal_separator, $thousands_separator);
}
/**
* Smarty regex_replace modifier plugin
* Type: modifier
* Name: regex_replace
* Purpose: regular expression search/replace
*
* @link https://www.smarty.net/manual/en/language.modifier.regex.replace.php
* regex_replace (Smarty online manual)
* @author Monte Ohrt <monte at ohrt dot com>
*
* @param string $string input string
* @param string|array $search regular expression(s) to search for
* @param string|array $replace string(s) that should be replaced
* @param int $limit the maximum number of replacements
*
* @return string
*/
private function smarty_modifier_regex_replace($string, $search, $replace, $limit = -1)
{
if (is_array($search)) {
foreach ($search as $idx => $s) {
$search[ $idx ] = $this->regex_replace_check($s);
}
} else {
$search = $this->regex_replace_check($search);
}
return preg_replace($search, $replace, $string, $limit);
}
/**
* @param string $search string(s) that should be replaced
*
* @return string
* @ignore
*/
private function regex_replace_check($search)
{
// null-byte injection detection
// anything behind the first null-byte is ignored
if (($pos = strpos($search, "\0")) !== false) {
$search = substr($search, 0, $pos);
}
// remove eval-modifier from $search
if (preg_match('!([a-zA-Z\s]+)$!s', $search, $match) && (strpos($match[ 1 ], 'e') !== false)) {
$search = substr($search, 0, -strlen($match[ 1 ])) . preg_replace('![e\s]+!', '', $match[ 1 ]);
}
return $search;
}
/**
* Smarty replace modifier plugin
* Type: modifier
* Name: replace
* Purpose: simple search/replace
*
* @link https://www.smarty.net/manual/en/language.modifier.replace.php replace (Smarty online manual)
* @author Monte Ohrt <monte at ohrt dot com>
* @author Uwe Tews
*
* @param string $string input string
* @param string $search text to search for
* @param string $replace replacement text
*
* @return string
*/
private function smarty_modifier_replace($string, $search, $replace)
{
return smarty_mb_str_replace($search, $replace, $string);
}
/**
* Smarty truncate modifier plugin
* Type: modifier
* Name: truncate
* Purpose: Truncate a string to a certain length if necessary,
* optionally splitting in the middle of a word, and
* appending the $etc string or inserting $etc into the middle.
*
* @link https://www.smarty.net/manual/en/language.modifier.truncate.php truncate (Smarty online manual)
* @author Monte Ohrt <monte at ohrt dot com>
*
* @param string $string input string
* @param integer $length length of truncated text
* @param string $etc end string
* @param boolean $break_words truncate at word boundary
* @param boolean $middle truncate in the middle of text
*
* @return string truncated string
*/
private function smarty_modifier_truncate($string, $length = 80, $etc = '...', $break_words = false, $middle = false)
{
if ($length === 0) {
return '';
}
if (mb_strlen($string, \Smarty\Smarty::$_CHARSET) > $length) {
$length -= min($length, mb_strlen($etc, \Smarty\Smarty::$_CHARSET));
if (!$break_words && !$middle) {
$string = preg_replace(
'/\s+?(\S+)?$/' . \Smarty\Smarty::$_UTF8_MODIFIER,
'',
mb_substr($string, 0, $length + 1, \Smarty\Smarty::$_CHARSET)
);
}
if (!$middle) {
return mb_substr($string, 0, $length, \Smarty\Smarty::$_CHARSET) . $etc;
}
return mb_substr($string, 0, intval($length / 2), \Smarty\Smarty::$_CHARSET) . $etc .
mb_substr($string, -intval($length / 2), $length, \Smarty\Smarty::$_CHARSET);
}
return $string;
}
}

View File

@@ -4,8 +4,12 @@ namespace Smarty\Extension;
interface ExtensionInterface {
public function getTagCompiler(string $tag): ?\Smarty\Compile\Tag\Base;
public function getTagCompiler(string $tag): ?\Smarty\Compile\Tag\TagCompilerInterface;
public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier\Base;
public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier\ModifierCompilerInterface;
public function getFunctionHandler(string $functionName): ?\Smarty\FunctionHandler\FunctionHandlerInterface;
public function getModifierCallback(string $modifierName);
}