-
Notifications
You must be signed in to change notification settings - Fork 711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix for issue 1011: add 3rd input encoding parameter to json_encode modifier and let it default to \Smarty\Smarty::$_CHARSET #1016
base: master
Are you sure you want to change the base?
Changes from 7 commits
4aedd6b
f4defd7
2f40db9
52878fe
77cac4e
e2b1d71
9316a42
ef2fa74
cbbb244
0027e97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,7 +31,6 @@ public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier | |
case 'indent': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\IndentModifierCompiler(); break; | ||
case 'is_array': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\IsArrayModifierCompiler(); break; | ||
case 'isset': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\IssetModifierCompiler(); break; | ||
case 'json_encode': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\JsonEncodeModifierCompiler(); break; | ||
case 'lower': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\LowerModifierCompiler(); break; | ||
case 'nl2br': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\Nl2brModifierCompiler(); break; | ||
case 'noprint': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\NoPrintModifierCompiler(); break; | ||
|
@@ -62,6 +61,7 @@ public function getModifierCallback(string $modifierName) { | |
case 'implode': return [$this, 'smarty_modifier_implode']; | ||
case 'in_array': return [$this, 'smarty_modifier_in_array']; | ||
case 'join': return [$this, 'smarty_modifier_join']; | ||
case 'json_encode': return [$this, 'smarty_modifier_json_encode']; | ||
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']; | ||
|
@@ -605,6 +605,98 @@ public function smarty_modifier_join($values, $separator = '') | |
return implode((string) ($separator ?? ''), (array) $values); | ||
} | ||
|
||
/** | ||
* Smarty json_encode modifier plugin. | ||
* Type: modifier | ||
* Name: json_encode | ||
* Purpose: Returns the JSON representation of the given value or false on error. The resulting string will be UTF-8 encoded. | ||
* | ||
* @param mixed $value | ||
* @param int $flags | ||
* @param string $input_encoding of $value; defaults to \Smarty\Smarty::$_CHARSET | ||
* | ||
* @return string|false | ||
*/ | ||
public function smarty_modifier_json_encode($value, $flags = 0, string $input_encoding = null) | ||
{ | ||
if (!$input_encoding) { | ||
$input_encoding = \Smarty\Smarty::$_CHARSET; | ||
} | ||
|
||
# json_encode() expects UTF-8 input, so recursively encode $value if necessary into UTF-8 | ||
if ($value && strcasecmp($input_encoding, 'UTF-8')) { | ||
if (is_string($value)) { # shortcut for the most common case | ||
$value = mb_convert_encoding($value, 'UTF-8', $input_encoding); | ||
} | ||
elseif (is_array($value) || is_object($value)) { | ||
static $transcoder; # this closure will be assigned once, and then persist in memory | ||
if (is_null($transcoder)) { | ||
/** | ||
* Similar to mb_convert_encoding(), but operates on keys and values of arrays, and on objects too. | ||
* Objects implementing \JsonSerializable and unsupported types are returned unchanged. | ||
* | ||
* @param string $from_encoding | ||
* @param string $to_encoding | ||
* @param mixed $data | ||
* @return mixed | ||
*/ | ||
$transcoder = function($data, string $to_encoding, string $from_encoding) use(&$transcoder) { | ||
if (empty($data)) { | ||
return $data; | ||
} | ||
elseif (is_string($data)) { | ||
return mb_convert_encoding($data, $to_encoding, $from_encoding); | ||
} | ||
elseif (is_scalar($data)) { | ||
return $data; | ||
} | ||
|
||
# convert object to array if necessary | ||
if (is_object($data)) { | ||
if (is_a($data, '\JsonSerializable')) { # this is the only reason why this function is not generic | ||
wisskid marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return $data; | ||
} | ||
$data = get_object_vars($data); # public properties as key => value pairs | ||
} | ||
|
||
if (is_array($data)) { | ||
$result = []; | ||
foreach ($data as $k => $v) { | ||
if (is_string($k)) { | ||
$k = mb_convert_encoding($k, $to_encoding, $from_encoding); | ||
if ($k === false) { | ||
return false; | ||
} | ||
} | ||
if (empty($v) || (is_scalar($v) && !is_string($v))) { | ||
$result[$k] = $v; # $v can be false and that's not an error | ||
} | ||
else { | ||
# recurse | ||
$v = $transcoder($v, $to_encoding, $from_encoding); | ||
if ($v === false) { | ||
return false; | ||
} | ||
$result[$k] = $v; | ||
} | ||
} | ||
return $result; | ||
} | ||
|
||
return $data; # anything except string, object, or array | ||
}; # / $transcoder function | ||
} # / if is_null($transcoder) | ||
|
||
$value = $transcoder($value, 'UTF-8', $input_encoding); | ||
if ($value === false) { | ||
return $value; # failure; this must not be passed to json_encode!; this is part of what the !empty() check is for at the top of this block | ||
} | ||
} # / elseif (is_array($value) || is_object($value)) | ||
} # / if input encoding != UTF-8 | ||
|
||
return \json_encode($value, $flags); # string|false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My IDE tells me that \json_encode (and \JsonSerializable::class) are not part of the core of PHP prior to PHP8. We should probably check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's strange that someone would want to use the |json_encode modifier without having the json extension installed. But what for exception (and message) to you suggest to throw if it's not present? The check can be done once (using a static boolean var) the first time the modifier is called. |
||
} | ||
|
||
/** | ||
* Smarty wordwrap modifier plugin | ||
* Type: modifier | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
<?php | ||
/** | ||
* Smarty PHPunit tests of modifier. | ||
* This file should be saved in UTF-8 encoding for comment legibility. | ||
*/ | ||
|
||
namespace UnitTests\TemplateSource\TagTests\PluginModifier; | ||
use PHPUnit_Smarty; | ||
|
||
class PluginModifierJsonEncodeCp1252Test extends PHPUnit_Smarty | ||
{ | ||
public function setUp(): void | ||
{ | ||
$this->setUpSmarty(__DIR__); | ||
\Smarty\Smarty::$_CHARSET = 'cp1252'; | ||
} | ||
|
||
public function tearDown(): void | ||
{ | ||
\Smarty\Smarty::$_CHARSET = 'UTF-8'; | ||
} | ||
|
||
/** | ||
* @dataProvider dataForDefault | ||
*/ | ||
public function testDefault($value, $expected) | ||
{ | ||
$tpl = $this->smarty->createTemplate('string:{$v|json_encode}'); | ||
$tpl->assign("v", $value); | ||
$this->assertEquals($expected, $this->smarty->fetch($tpl)); | ||
} | ||
|
||
/** | ||
* @dataProvider dataForDefault | ||
*/ | ||
public function testDefaultAsFunction($value, $expected) | ||
{ | ||
$tpl = $this->smarty->createTemplate('string:{json_encode($v)}'); | ||
$tpl->assign("v", $value); | ||
$this->assertEquals($expected, $this->smarty->fetch($tpl)); | ||
} | ||
|
||
public function dataForDefault() { | ||
return [ | ||
["abc", '"abc"'], | ||
[["abc"], '["abc"]'], | ||
[["abc",["a"=>2]], '["abc",{"a":2}]'], | ||
[["\x80uro",["Schl\xFCssel"=>"Stra\xDFe"]], '["\u20acuro",{"Schl\u00fcssel":"Stra\u00dfe"}]'], # x80 = � = euro, xFC = � = uuml, xDF = � = szlig | ||
]; | ||
} | ||
|
||
/** | ||
* @dataProvider dataForForceObject | ||
*/ | ||
public function testForceObject($value, $expected) | ||
{ | ||
$tpl = $this->smarty->createTemplate('string:{$v|json_encode:16}'); | ||
$tpl->assign("v", $value); | ||
$this->assertEquals($expected, $this->smarty->fetch($tpl)); | ||
} | ||
|
||
/** | ||
* @dataProvider dataForForceObject | ||
*/ | ||
public function testForceObjectAsFunction($value, $expected) | ||
{ | ||
$tpl = $this->smarty->createTemplate('string:{json_encode($v,16)}'); | ||
$tpl->assign("v", $value); | ||
$this->assertEquals($expected, $this->smarty->fetch($tpl)); | ||
} | ||
|
||
public function dataForForceObject() { | ||
return [ | ||
["abc", '"abc"'], | ||
[["abc"], '{"0":"abc"}'], | ||
[["abc",["a"=>2]], '{"0":"abc","1":{"a":2}}'], | ||
[["\x80uro"], '{"0":"\u20acuro"}'], | ||
]; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably did this because of this comment but I'm afraid this is a bit much. Can you refactor the transcoder into a class file under
src/Extension/DefaultExtension/DeepTranscoder.php
or something like that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, perhaps DeepJsonTranscode.php then since it's specific to transcoding for json_encode() as indicated by a comment in the code where JsonSerializable is checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just pushed a new commit where the json_encode() modifier uses the new class src/Extension/DefaultExtension/RecursiveTranscoder.php, and added a little to the json_encode unit tests.