When using PHP's json_encode to encode an array as a JSON string, is there any way at all to prevent the function from quoting specific values in the returned string? The reason I ask is because I need javascript to interpret certain values in the object as actual variable names, for example the name of an existing javascript function.
My end goal is to use the outputted json as the configuration object for an ExtJS Menu component, so the fact that everything gets quoted prevents me from successfully setting such properties as "handler" (click event handler function) of the child items arrays.
What we do is (and that's what Zend_Json::encode() does too), is to use a special marker class that encapsulates Javascript expressions in a special class. The encoding then walks recursively through our array-to-be-encoded, replaces all marker instances with some string. After using the built-in json_encode() we simply do a string replace to replace each special string with the __toString() value of the respective marker instance.
You can either use Zend_Json directly (if that's possible) or check how they do it and adapt the code to your needs.
Bill's function almost worked, it just needed the is_assoc() function added.
But while I was sorting this out, I cleaned it up a bit. This seems to work quite well for me:
<?php
/**
* JSObject class.
*/
class JSObject {
var $jsexp = 'JSEXP:';
/**
* is_assoc function.
*
* Determines whether or not the object is an associative array
*
* #access public
* #param mixed $arr
* #return boolean
*/
function is_assoc($arr) {
return (is_array($arr) && count(array_filter(array_keys($arr),'is_string')) == count($arr));
}
/**
* Encode object
*
* Encodes the object as a json string, parsing out items that were flagged as objects so that they are not wrapped in double quotes.
*
* #param array $properties
* #return string
*/
function encode($properties = array()) {
$is_assoc = $this->is_assoc($properties);
$enc_left = $is_assoc ? '{' : '[';
$enc_right = $is_assoc ? '}' : ']';
$outputArray = array();
foreach ($properties as $prop => $value) {
if ((is_array($value) && !empty($value)) || (is_string($value) && strlen(trim(str_replace($this->jsexp, '', $value))) > 0) || is_int($value) || is_float($value) || is_bool($value)) {
$output = (is_string($prop)) ? $prop.': ' : '';
if (is_array($value)) {
$output .= $this->encode($value);
}
else if (is_string($value)) {
$output .= (substr($value, 0, strlen($this->jsexp)) == $this->jsexp) ? substr($value, strlen($this->jsexp)) : json_encode($value);
}
else {
$output .= json_encode($value);
}
$outputArray[] = $output;
}
}
$fullOutput = implode(', ', $outputArray);
return $enc_left . $fullOutput . $enc_right;
}
/**
* JS expression
*
* Prefixes a string with the JS expression flag
* Strings with this flag will not be quoted by encode() so they are evaluated as expressions
*
* #param string $str
* #return string
*/
function js($str) {
return $this->jsexp.$str;
}
}
No, json_encode can't do that. You need to construct your JS expression by hand then:
$json = "{'special':" . json_encode($string) . " + js_var,"
. "'value': 123}";
(Try to still use json_encode for fixed value parts, like in above example.)
The json_encode function does not provide any functionality for controlling the quotes. The quotes are also necessary for JavaScript to properly form the object on the JavaScript side.
In order to use the returned value to construct an object on the JavaScript side, use the json_encoded string to set flags in your association.
For example:
json_encode( array( "click_handler"=> "FOO" ) );
JavaScript side in the AJAX:
if( json.click_handler == "FOO" ) {
json.click_handler = Your_Handler;
}
After these steps you can pass your object off somewhere.
my quickfix was this:
$myobject->withquotes = 'mystring';
$myobject->withoutquotes = '##noquote## mystring ##noquote##';
and later
str_replace(array('"##noquote## ', ' ##noquote##"'), '', json_encode($myobject))
result is something like this
{"withquotes":"mystring","withoutquotes":mystring}
This is what I ended up doing, which is pretty close to what Stefan suggested above I think:
class JSObject
{
var $jsexp = 'JSEXP:';
/**
* Encode object
*
*
* #param array $properties
* #return string
*/
function encode($properties=array())
{
$output = '';
$enc_left = $this->is_assoc($properties) ? '{' : '[';
$enc_right = ($enc_left == '{') ? '}' : ']';
foreach($properties as $prop => $value)
{
//map 'true' and 'false' string values to their boolean equivalent
if($value === 'true') { $value = true; }
if($value === 'false') { $value = false; }
if((is_array($value) && !empty($value)) || (is_string($value) && strlen(trim(str_replace($this->jsexp, '', $value))) > 0) || is_int($value) || is_float($value) || is_bool($value))
{
$output .= (is_string($prop)) ? $prop.': ' : '';
if(is_array($value))
{
$output .= $this->encode($value);
}
else if(is_string($value))
{
$output .= (substr($value, 0, strlen($this->jsexp)) == $this->jsexp) ? substr($value, strlen($this->jsexp)) : '\''.$value.'\'';
}
else if(is_bool($value))
{
$output .= ($value ? 'true' : 'false');
}
else
{
$output .= $value;
}
$output .= ',';
}
}
$output = rtrim($output, ',');
return $enc_left.$output.$enc_right;
}
/**
* JS expression
*
* Prefixes a string with the JS expression flag
* Strings with this flag will not be quoted by encode() so they are evaluated as expressions
*
* #param string $str
* #return string
*/
function js($str)
{
return $this->jsexp.$str;
}
}
Following the lead of Stefan Gehrig I put this rough little class together. Example below. One must remember to use the serialize method if one has used the mark method, otherwise the markers will persist in the final json.
class json_extended {
public static function mark_for_preservation($str) {
return 'OINK' . $str . 'OINK'; // now the oinks will be next to the double quotes
}
public static function serialize($stuff) {
$json = json_encode($stuff);
$json = str_replace(array('"OINK', 'OINK"'), '', $json);
return $json;
}
}
$js_arguments['submitHandler'] = json_extended::mark_for_preservation('handle_submit');
<script>
$("form").validate(<?=json_extended::serialize($js_arguments)?>);
// produces: $("form").validate({"submitHandler":handle_submit});
function handle_submit() {alert( 'Yay, pigs!'); }
</script>
Related
I have string like this in database (the actual string contains 100s of word and 10s of variable):
I am a {$club} fan
I echo this string like this:
$club = "Barcelona";
echo $data_base[0]['body'];
My output is I am a {$club} fan. I want I am a Barcelona fan. How can I do this?
Use strtr. It will translate parts of a string.
$club = "Barcelona";
echo strtr($data_base[0]['body'], array('{$club}' => $club));
For multiple values (demo):
$data_base[0]['body'] = 'I am a {$club} fan.'; // Tests
$vars = array(
'{$club}' => 'Barcelona',
'{$tag}' => 'sometext',
'{$anothertag}' => 'someothertext'
);
echo strtr($data_base[0]['body'], $vars);
Program Output:
I am a Barcelona fan.
I would suggest the sprintf() function.
Instead of storing I am a {$club} fan, use I am a %s fan, so your echo command would go like:
$club = "Barcelona";
echo sprintf($data_base[0]['body'],$club);
Output: I am a Barcelona fan
That would give you the freedom of use that same code with any other variable (and you don't even have to remember the variable name).
So this code is also valid with the same string:
$food = "French fries";
echo sprintf($data_base[0]['body'], $food);
Output: I am a French fries fan
$language = "PHP";
echo sprintf($data_base[0]['body'], $language);
Output: I am a PHP fan
/**
* A function to fill the template with variables, returns filled template.
*
* #param string $template A template with variables placeholders {$variable}.
* #param array $variables A key => value store of variable names and values.
*
* #return string
*/
public function replaceVariablesInTemplate($template, array $variables){
return preg_replace_callback('#{(.*?)}#',
function($match) use ($variables){
$match[1] = trim($match[1], '$');
return $variables[$match[1]];
},
' ' . $template . ' ');
}
Edit: This answer still gets upvotes, so people need to be aware that there's a security vulnerability in the naive interpolation technique present in the below code snippets. An adversary could include arbitrary variables in the input string which would reveal information about the server or other data in the runtime variable register. This is due to the way the general expression search is performed in that it finds any arbitrary variable name pattern, and then uses those variable names verbatim in the subsequent compact call. This causes clients to control server-side behavior similar to eval. I'm leaving this answer for posterity.
You are looking for nested string interpolation. A theory can be read in the blog post Wanted: PHP core function for dynamically performing double-quoted string variable interpolation.
The major problem is that you don't really know all of the variables available, or there may be too many to list.
Consider the following tested code snippet. I stole the regex from Mohammad Mohsenipur.
$testA = '123';
$testB = '456';
$testC = '789';
$t = '{$testA} adsf {$testB}adf 32{$testC} fddd{$testA}';
echo 'before: ' . $t . "\n";
preg_match_all('~\{\$(.*?)\}~si', $t, $matches);
if ( isset($matches[1])) {
$r = compact($matches[1]);
foreach ( $r as $var => $value ) {
$t = str_replace('{$' . $var . '}', $value, $t);
}
}
echo 'after: ' . $t . "\n";
Your code may be:
$club = 'Barcelona';
$tmp = $data_base[0]['body'];
preg_match_all('~\{\$(.*?)\}~si', $tmp, $matches);
if ( isset($matches[1])) {
$r = compact($matches[1]);
foreach ( $r as $var => $value ) {
$tmp = str_replace('{$' . $var . '}', $value, $tmp);
}
}
echo $tmp;
if (preg_match_all('#\$([a-zA-Z0-9]+)#', $q, $matches, PREG_SET_ORDER));
{
foreach ($matches as $m)
{
eval('$q = str_replace(\'' . $m[0] . '\', $' . $m[1] . ', $q);');
}
}
This matches all $variables and replaces them with the value.
I didn't include the {}'s, but it shouldn't be too hard to add them something like this...
if (preg_match_all('#\{\$([a-zA-Z0-9]+)\}#', $q, $matches, PREG_SET_ORDER));
{
foreach ($matches as $m)
{
eval('$q = str_replace(\'' . $m[0] . '\', $' . $m[1] . ', $q);');
}
}
Though it seems a bit slower than hard coding each variable. And it introduces a security hole with eval. That is why my regular expression is so limited. To limit the scope of what eval can grab.
I wrote my own regular expression tester with Ajax, so I could see, as I type, if my expression is going to work. I have variables I like to use in my expressions so that I don't need to retype the same bit for each expression.
I've found these approaches useful at times:
$name = 'Groot';
$string = 'I am {$name}';
echo eval('return "' . $string . '";');
$data = array('name' => 'Groot');
$string = 'I am {$data[name]}';
echo eval('return "' . $string . '";');
$name = 'Groot';
$data = (object)get_defined_vars();
$string = 'I am {$data->name}';
echo eval('return "' . $string . '";');
Here is my solution:
$club = "Barcelona";
$string = 'I am a {$club} fan';
preg_match_all("/\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/", $string, $matches);
foreach ($matches[0] as $key => $var_name) {
if (!isset($GLOBALS[$matches[1][$key]]))
$GLOBALS[$matches[1][$key]] = 'default value';
$string = str_replace($var_name, $GLOBALS[$matches[1][$key]], $string);
}
You can use a simple parser that replaces {$key} with a value from a map if it exists.
Use it like:
$text = templateWith('hello $item}', array('item' => 'world'...));`
My first version is:
/**
* Template with a string and simple map.
* #param string $template
* #param array $substitutions map of substitutions.
* #return string with substitutions applied.
*/
function templateWith(string $template, array $substitutions) {
$state = 0; // forwarding
$charIn = preg_split('//u', $template, -1, PREG_SPLIT_NO_EMPTY);
$charOut = array();
$count = count($charIn);
$key = array();
$i = 0;
while ($i < $count) {
$char = $charIn[$i];
switch ($char) {
case '{':
if ($state === 0) {
$state = 1;
}
break;
case '}':
if ($state === 2) {
$ks = join('', $key);
if (array_key_exists($ks, $substitutions)) {
$charOut[] = $substitutions[$ks];
}
$key = array();
$state = 0;
}
break;
case '$': if ($state === 1) {
$state = 2;
}
break;
case '\\': if ($state === 0) {
$i++;
$charOut[] = $charIn[$i];
}
continue;
default:
switch ($state) {
default:
case 0: $charOut[] = $char;
break;
case 2: $key[] = $char;
break;
}
}
$i++;
}
return join('', $charOut);
}
Maybe the following snippet is (partly) usefull for someone.
/**
* Access an object property using "dot" notation
*
* #param object $object
* #param string|null $path
* #param mixed $default
* #return mixed
*/
function xobject_get(object $object, $path, $default = null) {
return array_reduce(explode('.', $path), function ($o, $p) use ($default) {
return is_numeric($p) ? $o[$p] ?? $default : $o->$p ?? $default;
}, $object);
}
/**
* Access an array's property using "dot" notation
*
* #param array $array
* #param string|null $path
* #param mixed $default
* #return mixed
*/
function xarray_get(array $array, $path, $default = null) {
return array_reduce(explode('.', $path), function ($a, $p) use ($default) {
return $a[$p] ?? $default;
}, $array);
}
/**
* Replaces placeholders from a string with object or array values using "dot" notation
*
* Example:
* "The book {title} was written by {author.name}" becomes "The book Harry Potter was written by J.K. Rowling"
*
* #param array|object $data
* #param string $template
* #return string
*/
function render_template($data, string $template) {
preg_match_all("/\{([^\}]*)\}/", $template, $matches);
$replace = [];
foreach ($matches[1] as $param) {
$replace['{'.$param.'}'] = is_object($data) ? xobject_get($data, $param) : xarray_get($data, $param);
}
return strtr($template, $replace);
}
Try the preg_replace PHP function.
<?php
$club = "Barcelona";
echo $string = preg_replace('#\{.*?\}#si', $club, 'I am a {$club} fan');
?>
You can use preg_replace_callback for getting a variable name like:
$data_base[0]['body'] = preg_replace_callback(
'#{(.*?)}#',
function($m) {
$m[1] = trim($m[1], '$');
return $this->$m[1];
},
' ' . $data_base[0]['body'] . ' '
);
Attention: This code I wrote is for class($this);. You can declare a variable into the class. Then use this code for detecting the variables and replace them like:
<?php
class a {
function __construct($array) {
foreach($array as $key => $val) {
$this->$key = $val;
}
}
function replace($str){
return preg_replace_callback(
'#{(.*?)}#', function($m) {$m[1] = trim($m[1], '$'); return $this->$m[1];},
' ' . $str . ' ');
}
}
$obj = new a(array('club' => 3523));
echo $obj->replace('I am a {$club} fan');
Output:
I am a 3523 fan
For your case, honestly, I do not see a reason not to use eval :)
Here is some extra way to define your variables if they are too into your database:
$my_variable_name = 'club'; //coming from database
$my_value = 'Barcelona'; //coming from database
$my_msg= 'I am a {$club} fan'; //coming from database
$$my_variable_name = $my_value; // creating variable $club dinamically
$my_msg = eval("return \"$my_msg\";"); // eating the forbidden fruit
echo $my_msg; // prints 'I am Barcelona fan'
This code is fully tested and working with php 7.
But if you allow your users to define such strings into your database, better don't do it.
You should run eval only with trusted data.
Something like this should solve your problem:
$club = "Barcelona";
$var = 'I am a {$club} fan';
$res = preg_replace('/\{\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/e', "$$1", $var);
echo "$res\n";
It's a one-line preg_replace.
With PHP 5.5, /e modifier is deprecated. You can use a callback instead:
$club = "Barcelona";
$var = 'I am a {$club} fan';
$res = preg_replace_callback('/\{\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/',
create_function(
'$matches',
'extract($GLOBALS, EXTR_REFS | EXTR_SKIP); return $$matches[1];'),
$var);
echo "$res\n";
Note that this uses a hack of importing all global variables. This may not be exactly what you want. Possibly using closures would be a better idea.
I have string like this in database (the actual string contains 100s of word and 10s of variable):
I am a {$club} fan
I echo this string like this:
$club = "Barcelona";
echo $data_base[0]['body'];
My output is I am a {$club} fan. I want I am a Barcelona fan. How can I do this?
Use strtr. It will translate parts of a string.
$club = "Barcelona";
echo strtr($data_base[0]['body'], array('{$club}' => $club));
For multiple values (demo):
$data_base[0]['body'] = 'I am a {$club} fan.'; // Tests
$vars = array(
'{$club}' => 'Barcelona',
'{$tag}' => 'sometext',
'{$anothertag}' => 'someothertext'
);
echo strtr($data_base[0]['body'], $vars);
Program Output:
I am a Barcelona fan.
I would suggest the sprintf() function.
Instead of storing I am a {$club} fan, use I am a %s fan, so your echo command would go like:
$club = "Barcelona";
echo sprintf($data_base[0]['body'],$club);
Output: I am a Barcelona fan
That would give you the freedom of use that same code with any other variable (and you don't even have to remember the variable name).
So this code is also valid with the same string:
$food = "French fries";
echo sprintf($data_base[0]['body'], $food);
Output: I am a French fries fan
$language = "PHP";
echo sprintf($data_base[0]['body'], $language);
Output: I am a PHP fan
/**
* A function to fill the template with variables, returns filled template.
*
* #param string $template A template with variables placeholders {$variable}.
* #param array $variables A key => value store of variable names and values.
*
* #return string
*/
public function replaceVariablesInTemplate($template, array $variables){
return preg_replace_callback('#{(.*?)}#',
function($match) use ($variables){
$match[1] = trim($match[1], '$');
return $variables[$match[1]];
},
' ' . $template . ' ');
}
Edit: This answer still gets upvotes, so people need to be aware that there's a security vulnerability in the naive interpolation technique present in the below code snippets. An adversary could include arbitrary variables in the input string which would reveal information about the server or other data in the runtime variable register. This is due to the way the general expression search is performed in that it finds any arbitrary variable name pattern, and then uses those variable names verbatim in the subsequent compact call. This causes clients to control server-side behavior similar to eval. I'm leaving this answer for posterity.
You are looking for nested string interpolation. A theory can be read in the blog post Wanted: PHP core function for dynamically performing double-quoted string variable interpolation.
The major problem is that you don't really know all of the variables available, or there may be too many to list.
Consider the following tested code snippet. I stole the regex from Mohammad Mohsenipur.
$testA = '123';
$testB = '456';
$testC = '789';
$t = '{$testA} adsf {$testB}adf 32{$testC} fddd{$testA}';
echo 'before: ' . $t . "\n";
preg_match_all('~\{\$(.*?)\}~si', $t, $matches);
if ( isset($matches[1])) {
$r = compact($matches[1]);
foreach ( $r as $var => $value ) {
$t = str_replace('{$' . $var . '}', $value, $t);
}
}
echo 'after: ' . $t . "\n";
Your code may be:
$club = 'Barcelona';
$tmp = $data_base[0]['body'];
preg_match_all('~\{\$(.*?)\}~si', $tmp, $matches);
if ( isset($matches[1])) {
$r = compact($matches[1]);
foreach ( $r as $var => $value ) {
$tmp = str_replace('{$' . $var . '}', $value, $tmp);
}
}
echo $tmp;
if (preg_match_all('#\$([a-zA-Z0-9]+)#', $q, $matches, PREG_SET_ORDER));
{
foreach ($matches as $m)
{
eval('$q = str_replace(\'' . $m[0] . '\', $' . $m[1] . ', $q);');
}
}
This matches all $variables and replaces them with the value.
I didn't include the {}'s, but it shouldn't be too hard to add them something like this...
if (preg_match_all('#\{\$([a-zA-Z0-9]+)\}#', $q, $matches, PREG_SET_ORDER));
{
foreach ($matches as $m)
{
eval('$q = str_replace(\'' . $m[0] . '\', $' . $m[1] . ', $q);');
}
}
Though it seems a bit slower than hard coding each variable. And it introduces a security hole with eval. That is why my regular expression is so limited. To limit the scope of what eval can grab.
I wrote my own regular expression tester with Ajax, so I could see, as I type, if my expression is going to work. I have variables I like to use in my expressions so that I don't need to retype the same bit for each expression.
I've found these approaches useful at times:
$name = 'Groot';
$string = 'I am {$name}';
echo eval('return "' . $string . '";');
$data = array('name' => 'Groot');
$string = 'I am {$data[name]}';
echo eval('return "' . $string . '";');
$name = 'Groot';
$data = (object)get_defined_vars();
$string = 'I am {$data->name}';
echo eval('return "' . $string . '";');
Here is my solution:
$club = "Barcelona";
$string = 'I am a {$club} fan';
preg_match_all("/\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/", $string, $matches);
foreach ($matches[0] as $key => $var_name) {
if (!isset($GLOBALS[$matches[1][$key]]))
$GLOBALS[$matches[1][$key]] = 'default value';
$string = str_replace($var_name, $GLOBALS[$matches[1][$key]], $string);
}
You can use a simple parser that replaces {$key} with a value from a map if it exists.
Use it like:
$text = templateWith('hello $item}', array('item' => 'world'...));`
My first version is:
/**
* Template with a string and simple map.
* #param string $template
* #param array $substitutions map of substitutions.
* #return string with substitutions applied.
*/
function templateWith(string $template, array $substitutions) {
$state = 0; // forwarding
$charIn = preg_split('//u', $template, -1, PREG_SPLIT_NO_EMPTY);
$charOut = array();
$count = count($charIn);
$key = array();
$i = 0;
while ($i < $count) {
$char = $charIn[$i];
switch ($char) {
case '{':
if ($state === 0) {
$state = 1;
}
break;
case '}':
if ($state === 2) {
$ks = join('', $key);
if (array_key_exists($ks, $substitutions)) {
$charOut[] = $substitutions[$ks];
}
$key = array();
$state = 0;
}
break;
case '$': if ($state === 1) {
$state = 2;
}
break;
case '\\': if ($state === 0) {
$i++;
$charOut[] = $charIn[$i];
}
continue;
default:
switch ($state) {
default:
case 0: $charOut[] = $char;
break;
case 2: $key[] = $char;
break;
}
}
$i++;
}
return join('', $charOut);
}
Maybe the following snippet is (partly) usefull for someone.
/**
* Access an object property using "dot" notation
*
* #param object $object
* #param string|null $path
* #param mixed $default
* #return mixed
*/
function xobject_get(object $object, $path, $default = null) {
return array_reduce(explode('.', $path), function ($o, $p) use ($default) {
return is_numeric($p) ? $o[$p] ?? $default : $o->$p ?? $default;
}, $object);
}
/**
* Access an array's property using "dot" notation
*
* #param array $array
* #param string|null $path
* #param mixed $default
* #return mixed
*/
function xarray_get(array $array, $path, $default = null) {
return array_reduce(explode('.', $path), function ($a, $p) use ($default) {
return $a[$p] ?? $default;
}, $array);
}
/**
* Replaces placeholders from a string with object or array values using "dot" notation
*
* Example:
* "The book {title} was written by {author.name}" becomes "The book Harry Potter was written by J.K. Rowling"
*
* #param array|object $data
* #param string $template
* #return string
*/
function render_template($data, string $template) {
preg_match_all("/\{([^\}]*)\}/", $template, $matches);
$replace = [];
foreach ($matches[1] as $param) {
$replace['{'.$param.'}'] = is_object($data) ? xobject_get($data, $param) : xarray_get($data, $param);
}
return strtr($template, $replace);
}
Try the preg_replace PHP function.
<?php
$club = "Barcelona";
echo $string = preg_replace('#\{.*?\}#si', $club, 'I am a {$club} fan');
?>
You can use preg_replace_callback for getting a variable name like:
$data_base[0]['body'] = preg_replace_callback(
'#{(.*?)}#',
function($m) {
$m[1] = trim($m[1], '$');
return $this->$m[1];
},
' ' . $data_base[0]['body'] . ' '
);
Attention: This code I wrote is for class($this);. You can declare a variable into the class. Then use this code for detecting the variables and replace them like:
<?php
class a {
function __construct($array) {
foreach($array as $key => $val) {
$this->$key = $val;
}
}
function replace($str){
return preg_replace_callback(
'#{(.*?)}#', function($m) {$m[1] = trim($m[1], '$'); return $this->$m[1];},
' ' . $str . ' ');
}
}
$obj = new a(array('club' => 3523));
echo $obj->replace('I am a {$club} fan');
Output:
I am a 3523 fan
For your case, honestly, I do not see a reason not to use eval :)
Here is some extra way to define your variables if they are too into your database:
$my_variable_name = 'club'; //coming from database
$my_value = 'Barcelona'; //coming from database
$my_msg= 'I am a {$club} fan'; //coming from database
$$my_variable_name = $my_value; // creating variable $club dinamically
$my_msg = eval("return \"$my_msg\";"); // eating the forbidden fruit
echo $my_msg; // prints 'I am Barcelona fan'
This code is fully tested and working with php 7.
But if you allow your users to define such strings into your database, better don't do it.
You should run eval only with trusted data.
Something like this should solve your problem:
$club = "Barcelona";
$var = 'I am a {$club} fan';
$res = preg_replace('/\{\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/e', "$$1", $var);
echo "$res\n";
It's a one-line preg_replace.
With PHP 5.5, /e modifier is deprecated. You can use a callback instead:
$club = "Barcelona";
$var = 'I am a {$club} fan';
$res = preg_replace_callback('/\{\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/',
create_function(
'$matches',
'extract($GLOBALS, EXTR_REFS | EXTR_SKIP); return $$matches[1];'),
$var);
echo "$res\n";
Note that this uses a hack of importing all global variables. This may not be exactly what you want. Possibly using closures would be a better idea.
I'm using MariaDB's COLUMN_JSON() function. As this bug illustrates, the function properly escapes double quotes, but not other characters that should be encoded/escaped.
Here's a silly example query to demonstrate how the JSON column is created.
SELECT CONCAT('[', GROUP_CONCAT(COLUMN_JSON(COLUMN_CREATE(
'name', `name`,
'value', `value`
)) SEPARATOR ','), ']') AS `json`
FROM `settings`
If the name or value contain invalid JSON characters, json_decode will fail.
I've written a PHP function to escape/encode the value that comes from the query, but it seems like there should be a better way.
/**
* Makes sure the JSON values built by COLUMN_JSON() in MariaDB are safe for json_decode()
* Assumes that double quotes are already escaped
*
* #param string $mysql_json
* #return string
*/
public static function jsonEscape($mysql_json)
{
$rtn = '';
for ($i = 0; $i < strlen($mysql_json); ++$i) {
$char = $mysql_json[$i];
if (($char === '\\') && ($mysql_json[$i + 1] !== '"')) {
// escape a backslash, but leave escaped double quotes intact
$rtn .= '\\\\';
} elseif (($ord = ord($char)) && ($ord < 32)) {
// hex encode control characters (below ASCII 32)
$rtn .= '\\u' . str_pad(dechex($ord), 4, '0', STR_PAD_LEFT);
} else {
$rtn .= $char;
}
}
return $rtn;
}
Examine the string character-by-character like this doesn't perform well. Perhaps there's a string replacement or regular expression that would be more performant?
Based on a comment from Halcyon, I switched to a str_replace() solution, and it performs much better! The performance difference between trim(json_encode(13), '"') and '\\u' . str_pad(dechex(13), 4, '0', STR_PAD_LEFT) is just barely better, but it makes the intent more clear.
private static $json_replace_search;
private static $json_replace_replace;
/**
* Makes sure the JSON values built by GROUP_CONCAT() and COLUMN_JSON() in MariaDB are safe for json_decode()
* Assumes that double quotes are already escaped
*
* #param string $mysql_json
* #return string
*/
public static function jsonEscape($mysql_json)
{
if (is_null(self::$json_replace_search)) {
// initialize
self::$json_replace_search = [];
self::$json_replace_replace = [];
// set up all of the control characters (below ASCII 32)
for ($i = 0; $i < 32; ++$i) {
self::$json_replace_search[$i] = chr($i);
self::$json_replace_replace[$i] = trim(json_encode(self::$json_replace_search[$i]), '"');
}
}
// replace them
return str_replace(self::$json_replace_search, self::$json_replace_replace, $mysql_json);
}
/**
*
* #param string $mysql_json
* #return mixed
*/
public static function jsonDecode($mysql_json)
{
return json_decode(self::jsonEscape($mysql_json));
}
Now php can't work directly wit Postgresql array. For example, php taking postgresql array like
'{"foo","bar"}'
I need simple php function to create multidimensional postgresql array from php array.
I think that experimental pg_convert() isn't optimal because it needs of extra data to form simple array string for database output, maybe I misunderstood the idea of this function.
For example, I need to convert
$from=array( array( "par_1_1","par_1_2" ), array( "array_2_1", "array_2_2" ) );
$to='{{"par_1_1","par_1_2"},{"par_2_1","par_2_2"}}';
Can I use array_walk_recursive() to convert the deepest elements of array?
Here's a simple function for converting a PHP array to PG array.
function to_pg_array($set) {
settype($set, 'array'); // can be called with a scalar or array
$result = array();
foreach ($set as $t) {
if (is_array($t)) {
$result[] = to_pg_array($t);
} else {
$t = str_replace('"', '\\"', $t); // escape double quote
if (! is_numeric($t)) // quote only non-numeric values
$t = '"' . $t . '"';
$result[] = $t;
}
}
return '{' . implode(",", $result) . '}'; // format
}
A little edit that uses pg_escape_string for quoting and that support PHP NULLS and booleans:
/**
* Converts a php array into a postgres array (also multidimensional)
*
* Each element is escaped using pg_escape_string, only string values
* are enclosed within single quotes, numeric values no; special
* elements as php nulls or booleans are literally converted, so the
* php NULL value is written literally 'NULL' and becomes a postgres
* NULL (the same thing is done with TRUE and FALSE values).
*
* Examples :
* VARCHAR VERY BASTARD ARRAY :
* $input = array('bla bla', 'ehi "hello"', 'abc, def', ' \'VERY\' "BASTARD,\'value"', NULL);
*
* to_pg_array($input) ==>> 'ARRAY['bla bla','ehi "hello"','abc, def',' ''VERY'' "BASTARD,''value"',NULL]'
*
* try to put this value in a query (you will get a valid result):
* select unnest(ARRAY['bla bla','ehi "hello"','abc, def',' ''VERY'' "BASTARD,''value"',NULL]::varchar[])
*
* NUMERIC ARRAY:
* $input = array(1, 2, 3, 8.5, null, 7.32);
* to_pg_array($input) ==>> 'ARRAY[1,2,3,8.5,NULL,7.32]'
* try: select unnest(ARRAY[1,2,3,8.5,NULL,7.32]::numeric[])
*
* BOOLEAN ARRAY:
* $input = array(false, true, true, null);
* to_pg_array($input) ==>> 'ARRAY[FALSE,TRUE,TRUE,NULL]'
* try: select unnest(ARRAY[FALSE,TRUE,TRUE,NULL]::boolean[])
*
* MULTIDIMENSIONAL ARRAY:
* $input = array(array('abc', 'def'), array('ghi', 'jkl'));
* to_pg_array($input) ==>> 'ARRAY[ARRAY['abc','def'],ARRAY['ghi','jkl']]'
* try: select ARRAY[ARRAY['abc','def'],ARRAY['ghi','jkl']]::varchar[][]
*
* EMPTY ARRAY (is different than null!!!):
* $input = array();
* to_pg_array($input) ==>> 'ARRAY[]'
* try: select unnest(ARRAY[]::varchar[])
*
* NULL VALUE :
* $input = NULL;
* to_pg_array($input) ==>> 'NULL'
* the functions returns a string='NULL' (literally 'NULL'), so putting it
* in the query, it becomes a postgres null value.
*
* If you pass a value that is not an array, the function returns a literal 'NULL'.
*
* You should put the result of this functions directly inside a query,
* without quoting or escaping it and you cannot use this result as parameter
* of a prepared statement.
*
* Example:
* $q = 'INSERT INTO foo (field1, field_array) VALUES ($1, ' . to_pg_array($php_array) . '::varchar[])';
* $params = array('scalar_parameter');
*
* It is recommended to write the array type (ex. varchar[], numeric[], ...)
* because if the array is empty or contains only null values, postgres
* can give an error (cannot determine type of an empty array...)
*
* The function returns only a syntactically well-formed array, it does not
* make any logical check, you should consider that postgres gives errors
* if you mix different types (ex. numeric and text) or different dimensions
* in a multidim array.
*
* #param array $set PHP array
*
* #return string Array in postgres syntax
*/
function to_pg_array($set) {
if (is_null($set) || !is_array($set)) {
return 'NULL';
}
// can be called with a scalar or array
settype($set, 'array');
$result = array();
foreach ($set as $t) {
// Element is array : recursion
if (is_array($t)) {
$result[] = to_pg_array($t);
}
else {
// PHP NULL
if (is_null($t)) {
$result[] = 'NULL';
}
// PHP TRUE::boolean
elseif (is_bool($t) && $t == TRUE) {
$result[] = 'TRUE';
}
// PHP FALSE::boolean
elseif (is_bool($t) && $t == FALSE) {
$result[] = 'FALSE';
}
// Other scalar value
else {
// Escape
$t = pg_escape_string($t);
// quote only non-numeric values
if (!is_numeric($t)) {
$t = '\'' . $t . '\'';
}
$result[] = $t;
}
}
}
return 'ARRAY[' . implode(",", $result) . ']'; // format
}
This is the same as mstefano80's answer, but more human-readable, universal and modern (at least for me):
<?php
class Sql
{
/**
* Convert PHP-array to SQL-array
* https://stackoverflow.com/questions/5631387/php-array-to-postgres-array
*
* #param array $data
* #return string
*/
public static function toArray(array $data, $escape = 'pg_escape_string')
{
$result = [];
foreach ($data as $element) {
if (is_array($element)) {
$result[] = static::toArray($element, $escape);
} elseif ($element === null) {
$result[] = 'NULL';
} elseif ($element === true) {
$result[] = 'TRUE';
} elseif ($element === false) {
$result[] = 'FALSE';
} elseif (is_numeric($element)) {
$result[] = $element;
} elseif (is_string($element)) {
$result[] = "'" . $escape($element) . "'";
} else {
throw new \InvalidArgumentException("Unsupported array item");
}
}
return sprintf('ARRAY[%s]', implode(',', $result));
}
}
Tests:
<?php
use Sql;
class SqlTest extends \PHPUnit_Framework_TestCase
{
public function testToArray()
{
$this->assertSame("ARRAY['foo','bar']", Sql::toArray(['foo', 'bar']));
$this->assertSame("ARRAY[1,2]", Sql::toArray([1, 2]));
$this->assertSame("ARRAY[1,2]", Sql::toArray(['1', '2']));
$this->assertSame("ARRAY['foo\\\"bar','bar\'foo']", Sql::toArray(['foo"bar', 'bar\'foo'], function($str){
return addslashes($str);
}));
$this->assertSame("ARRAY[ARRAY['foo\\\"bar'],ARRAY['bar\'foo']]", Sql::toArray([['foo"bar'], ['bar\'foo']], function($str){
return addslashes($str);
}));
}
}
Are Zend_Db_Select's where() method, when including the optional value to quite into, and Zend_Db_Adapte's quoteInto() methods basically the same as far as escaping SQL?
In other words, are these two pieces of quote identical and equally secure?
$select->where($this->getAdapter()->quoteInto('id = ?', 3));
$select->where(id = ?, 3);
Thanks!
Zend_Db_Select::_where() is using Zend_Db_Abstract::quoteInto() to quote the value(s) you specify as the second parameter in Zend_Db_Select::where() when assembling the sql string.
From line 983 of Zend_Db_Select:
/**
* Internal function for creating the where clause
*
* #param string $condition
* #param mixed $value optional
* #param string $type optional
* #param boolean $bool true = AND, false = OR
* #return string clause
*/
protected function _where($condition, $value = null, $type = null, $bool = true)
{
if (count($this->_parts[self::UNION])) {
require_once 'Zend/Db/Select/Exception.php';
throw new Zend_Db_Select_Exception("Invalid use of where clause with " . self::SQL_UNION);
}
if ($value !== null) {
$condition = $this->_adapter->quoteInto($condition, $value, $type);
}
$cond = "";
if ($this->_parts[self::WHERE]) {
if ($bool === true) {
$cond = self::SQL_AND . ' ';
} else {
$cond = self::SQL_OR . ' ';
}
}
return $cond . "($condition)";
}
As I understand it where does this already so specifying it would be redundant.