PDO PHP bindValue doesn't work - php

I know this has been asked 1000 times, but for some reason I continue to bang my head agains the wall..
This works:
$sql = 'SELECT a.eventCode, a.eventTime, a.teamCode, a.playerCode, b.lastName, b.firstName, b.number, a.xCoord, a.yCoord, a.id ';
$sql = $sql . 'FROM events a, players b ';
$sql = $sql . 'WHERE a.regGUID in ( ' . $regGUID . ' ) and ';
$sql = $sql . 'a.playerCode=b.playerCode and a.gameCode = "' . $game . '" order by a.eventTime desc, a.actionCode asc';
$stmt = $db->prepare($sql);
$results = $stmt->execute();
This Doesn't:
$sql = 'SELECT a.eventCode, a.eventTime, a.teamCode, a.playerCode, b.lastName, b.firstName, b.number, a.xCoord, a.yCoord, a.id ';
$sql = $sql . 'FROM events a, players b ';
$sql = $sql . 'WHERE a.regGUID in ( :regGUID ) and ';
$sql = $sql . 'a.playerCode=b.playerCode and a.gameCode = :game order by a.eventTime desc, a.actionCode asc';
$stmt = $db->prepare($sql);
$stmt->bindValue(':regGUID', $regGUID, PDO::PARAM_STR);
$stmt->bindValue(':game', $game, PDO::PARAM_STR);
$results = $stmt->execute();
What am I missing? Thanks

The problem is here:
$sql = $sql . 'WHERE a.regGUID in ( :regGUID ) and ';
$stmt->bindValue(':regGUID', $regGUID, PDO::PARAM_STR);
I assume $regGUID is a comma-separated list of quoted strings.
Each query parameter accepts only a single scalar value. Not lists of values.
So you have two choices:
Continue to interpolate the $regGUID string, even if you use parameters for other scalar values. But you still want to be careful to avoid SQL injection, so you must form the $regGUID string correctly. You can't just call PDO::quote() on the whole string, that would make it a single quoted string containing UUIDs and commas. You have to make sure each UUID string is escaped and quoted individually, then implode the list together and interpolate it into the IN clause.
$regGUIDs = explode(',', $regGUID);
$regGUIDs = array_map(function ($g) { return $db->quote($g); }, $regGUIDs);
$regGUID = implode(',', $regGUIDs);
$sql = $sql . 'WHERE a.regGUID in (' . $regGUID . ') and ';
explode() the $regGUID into an array, and add one query parameter for each element in the array. Interpolate the dynamic list of query parameter placeholders.
$regGUIDs = explode(',', $regGUID);
$params = array_fill(1, count($regGUIDs), '?');
$sql = $sql . ' WHERE a.regGUID in ( ' . implode(',', $params) . ' ) and ';
You could bindValue() in a loop for the array, but keep in mind that other parameters should also be bound by position, not by name. PDO has bugs that make it not happy when you try to mix the two different styles of parameters in the same query.
Instead of using bindValue() I just pass an array of parameter values to PDOStatement::execute(), which is much easier.
$paramValues = $regGUIDs;
$paramValues[] = $game;
$results = $stmt->execute($paramValues);

This indeed has been asked 1000 times.
Prepared statements can only accept scalar values, not arbitrary parts of the SQL query.
You have to form IN() statement using as many placeholders, as many items you have to put in and then bind them one by one.
To ease this task one can use some helper function.
Say, using SafeMysql library this code could be written as
$sql = 'SELECT * FROM events a, players b WHERE regGUID in (?a) and';
$sql .= ' a.playerCode=b.playerCode and a.gameCode = ?s';
$sql .= ' order by a.eventTime desc, a.actionCode asc';
$results = $db->getAll($sql,$regGUID,$game);
Note that $regGUID should be an array, not string and $results already contain all the requested data, without any further processing.

What is the content of $regGUID? Since you're using an in clause, I suspect a comma separated list.
Binding a variable to a parameter is not like substituting that string into the query; it is like telling MySQL how to use your actual PHP variables. So if you bind a string like '1,2,3' to the query parameter, it stays as one string, and is not reinterpreted as a list of numbers.
Consequently, if $regGUID is something like "'AAA1', 'BBB2'", your first query becomes
... WHERE a.regGUID in ( 'AAA1', 'BBB2' ) ...
but your second query is more like
... WHERE a.regGUID in ( '\'AAA1\', \'BBB2\'' ) ...
which is the same as saying
... WHERE a.regGUID = '\'AAA1\', \'BBB2\'' ...

As others have state you can only bind a single scalar value to a placeholder. So this means you actually need a placeholder for each value in your IN statement. I normally do something like the following. It should be noted though that i never use bindValue so if it has rules about things having to be references like Mysqli then the below may need to be modified:
$regGUIDPlaceholders = array();
// prepare the placeholders
// assume regGUID is an array - if its a string then explode on whatever to make it an array
foreach($regGUID as $k => $v) {
$placeholder = ':regGUID' . $k;
$regGUIDPlaceholders[$key] = $value;
}
// prepare the IN statememnt
$in = sprintf('IN (%s)', implode(',', array_keys($regGUIDPlaceholders)));
$sql = 'SELECT a.eventCode, a.eventTime, a.teamCode, a.playerCode, b.lastName, b.firstName, b.number, a.xCoord, a.yCoord, a.id ';
$sql = $sql . 'FROM events a, players b ';
// USE the IN statement dynamically prepared above
$sql = $sql . 'WHERE a.regGUID '. $in . ' and ';
$sql = $sql . 'a.playerCode=b.playerCode and a.gameCode = :game order by a.eventTime desc, a.actionCode asc';
$stmt = $db->prepare($sql);
// bind each GUID to its placeholder
foreach($regGUIDPlaceholders as $placeholder => $value) {
$stmt->bindValue($placeholder, $value, PDO::PARAM_STR);
}
$stmt->bindValue(':game', $game, PDO::PARAM_STR);
$results = $stmt->execute();

Related

PDO: Delete query only deleting entries of first value in IN statement [duplicate]

I found this code on SO, which is great for using PDO and the IN() statement together.
$values = explode(',', $values) ; # 1,4,7
$placeholders = rtrim(str_repeat('?, ', count($values)), ', ') ;
$query = "SELECT * FROM table WHERE id IN ($placeholders)";
$stm = $db->prepare($query) ;
$stm->execute($values) ;
However, how can I mix in another addition to the query so the query looks like this:
$query = "SELECT * FROM table WHERE id IN ($placeholders) AND product=?";
$stm = $db->prepare($query) ;
$stm->execute(array($values,$product)) ; //error happens when adding product placeholder
I thought this would work but I get:
Warning: PDOStatement::execute() [pdostatement.execute]: SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens in line 3 (the $stm line)
Any idea how to get this to behave as intended?
UPDATED execute to array, still not working..
Solution
This should work, if $values is an array:
$query = "SELECT * FROM table WHERE id IN ($placeholders) AND product=?";
$stm->execute(array_merge($values, array($product)));
Explanation
execute() expects one parameter - in this case an array - to be provided. By adding array_merge($values, array($product)) you create one array with $product added at the end, so the query should work correctly.
See the demo here: http://ideone.com/RcClX
$stm->execute($values,$product) ; //error happens when adding product placeholder
The problem here is that execute needs a single array. You can't pass multiple arrays, and worse, you can't nest arrays.
We already have a perfectly good $values array, so let's reuse it after you create the placeholder string.
$values = explode(',', $values) ; # 1,4,7
$placeholders = rtrim(str_repeat('?, ', count($values)), ', ') ;
$query = "SELECT * FROM table WHERE id IN ($placeholders) AND product=?";
// New!
$values[] = $product;
$stm = $db->prepare($query);
$stm->execute($values);
And an other solution can be (if you like the :param_name = $value way, as me):
$params = array(
':product' => $product
);
$_in_params = array();
foreach ( $_in_values as $idx_in => $value_in)
{
$_in_params[] = ':param_in_'.$idx_in;
$params[':param_in_'.$idx_in] = $value_in;
}
$query .= "SELECT * FROM table WHERE id IN (".join(',',$_in_params).") AND product=:product";
I'm not sure if this is the best and the most optimal solution, but it's a little bit more human readable :) And it can be helpful if you have a big an complicated query and you want to debug it
(I'm curious if someone have a good argument why NOT to do in this way)
You forgot to prepare it ^_^
$query = "SELECT * FROM table WHERE id IN ($placeholders) AND product=?";
$stm = $db->prepare($query) ;
$stm->execute($values,$product) ; //p00f
And aside from that execute() should only have one parameter
So the above won't work AT ALL!
See the DOCs
Placeholders version if you need it
$values = [1, 4, 7, 8];
$placeholders = preg_filter('/^/', ':prefix_', array_keys($values)));
$query = 'SELECT * FROM table WHERE id IN ( '. implode(', ', $placeholders) . ')';
$stmt = $db->prepare($query);
if (count($values) > 0) {
foreach ($values as $key => $current_value) {
$stmt->bindValue($placeholders[$key] , $current_value, PDO::PARAM_STR);
}
}
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

How to add an optional condition to a prepared statement? [duplicate]

I need to change this query to use a prepared statement. Is it possible?
The query:
$sql = "SELECT id, title, content, priority, date, delivery FROM tasks " . $op . " " . $title . " " . $content . " " . $priority . " " . $date . " " . $delivery . " ORDER BY " . $orderField . " " . $order . " " . $pagination . "";
Before the query, there's code to check the POST variables and change the content of variables in the query.
//For $op makes an INNER JOIN with or without IN clause depending on the content of a $_POST variable
$op = "INNER JOIN ... WHERE opID IN ('"$.opID."')";
//Or
$op = "INNER JOIN ... ";
//For $title (depends of $op):
$title = "WHERE title LIKE'%".$_POST["title"]."%'";
//Or
$title = "AND title LIKE'%".$_POST["title"]."%'";
//For $content:
$content = "AND content LIKE '%".$_POST["content"]."%'";
//For $priority just a switch:
$priority = "AND priority = DEPENDING_CASE";
//For $date and $delivery another switch
$d = date("Y-m-d", strtotime($_POST["date"]));
$date = "AND date >= '$d' 00:00:00 AND date <= '$d' 23:59:59";
//Or $date = "AND date >= '$d' 00:00:00";
//Or $date = "AND date <= '$d' 23:59:59";
//For $orderField
$orderField = $_POST["column"];
//For $order
$order= $_POST["order"];
//For $pagination
$pagination = "LIMIT ".$offset.",". $recordsPerPage;
How I could do this query using prepared statement?
The query could be more static but this means to make different prepared statements and execute it depending of $_POST checks.
It depends on many variables because this query show results in a table that contains search fields and column to order.
A full example of query would be like this (depending of $_POST checks):
SELECT id, title, content, priority, date, delivery FROM tasks INNER JOIN op ON task.op = op.opId WHERE op IN (4851,8965,78562) AND title LIKE '%PHT%' AND content LIKE '%%' AND priority = '2' ORDER BY date DESC LIMIT 0, 10
An excellent question. And thank you for moving to prepared statements. It seems that after all those years of struggle, the idea finally is starting to take over.
Disclaimer: there will be links to my own site because I am helping people with PHP for 20+ years and got an obsession with writing articles about most common issues.
Yes, it's perfectly possible. Check out my article, How to create a search filter for mysqli for the fully functional example.
For the WHERE part, all you need is to create two separate arrays - one containing query conditions with placeholders and one containing actual values for these placeholders, i.e:
WHERE clause
$conditions = [];
$parameters = [];
if (!empty($_POST["content"])) {
$conditions[] = 'content LIKE ?';
$parameters[] = '%'.$_POST['content ']."%";
}
and so on, for all search conditions.
Then you could implode all the conditions using AND string as a glue, and get a first-class WHERE clause:
if ($conditions)
{
$where .= " WHERE ".implode(" AND ", $conditions);
}
The routine is the same for all search conditions, but it will be a bit different for the IN() clause.
IN() clause
is a bit different as you will need more placeholders and more values to be added:
if (!empty($_POST["opID"])) {
$in = str_repeat('?,', count($array) - 1) . '?';
$conditions[] = "opID IN ($in)";
$parameters = array_merge($parameters, $_POST["opID"]);
}
this code will add as many ? placeholders to the IN() clause as many elements in the $_POST["opID"] and will add all those values to the $parameters array. The explanation can be found in the adjacent article in the same section on my site.
After you are done with WHERE clause, you can move to the rest of your query
ORDER BY clause
You cannot parameterize the order by clause, because field names and SQL keywords cannot be represented by a placeholder. And to tackle with this problem I beg you to use a whitelisting function I wrote for this exact purpose. With it you can make your ORDER BY clause 100% safe but perfectly flexible. All you need is to predefine an array with field names allowed in the order by clause:
$sortColumns = ["title","content","priority"]; // add your own
and then get safe values using this handy function:
$orderField = white_list($_POST["column"], $sortColumns, "Invalid column name");
$order = white_list($_POST["order"], ["ASC","DESC"], "Invalid ORDER BY direction");
this is a smart function, that covers three different scenarios
in case no values were provided (i.e. $_POST["column"] is empty) the first value from the white list will be used, so it serves as a default value
in case a correct value provided, it will be used in the query
in case an incorrect value is provided, then an error will be thrown.
LIMIT clause
LIMIT values are perfectly parameterized so you can just add them to the $parameters array:
$limit = "LIMIT ?, ?";
$parameters[] = $offset;
$parameters[] = $recordsPerPage;
The final assembly
In the end, your query will be something like this
$sql = "SELECT id, title, content, priority, date, delivery
FROM tasks INNER JOIN ... $where ORDER BY `$orderField` $order $limit";
And it can be executed using the following code
$stmt = $mysqli->prepare($sql);
$stmt->bind_param(str_repeat("s", count($parameters)), ...$parameters);
$stmt->execute();
$data = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
where $data is a conventional array contains all the rows returned by the query.

How to make a fully dynamic prepared statement using mysqli API?

I need to change this query to use a prepared statement. Is it possible?
The query:
$sql = "SELECT id, title, content, priority, date, delivery FROM tasks " . $op . " " . $title . " " . $content . " " . $priority . " " . $date . " " . $delivery . " ORDER BY " . $orderField . " " . $order . " " . $pagination . "";
Before the query, there's code to check the POST variables and change the content of variables in the query.
//For $op makes an INNER JOIN with or without IN clause depending on the content of a $_POST variable
$op = "INNER JOIN ... WHERE opID IN ('"$.opID."')";
//Or
$op = "INNER JOIN ... ";
//For $title (depends of $op):
$title = "WHERE title LIKE'%".$_POST["title"]."%'";
//Or
$title = "AND title LIKE'%".$_POST["title"]."%'";
//For $content:
$content = "AND content LIKE '%".$_POST["content"]."%'";
//For $priority just a switch:
$priority = "AND priority = DEPENDING_CASE";
//For $date and $delivery another switch
$d = date("Y-m-d", strtotime($_POST["date"]));
$date = "AND date >= '$d' 00:00:00 AND date <= '$d' 23:59:59";
//Or $date = "AND date >= '$d' 00:00:00";
//Or $date = "AND date <= '$d' 23:59:59";
//For $orderField
$orderField = $_POST["column"];
//For $order
$order= $_POST["order"];
//For $pagination
$pagination = "LIMIT ".$offset.",". $recordsPerPage;
How I could do this query using prepared statement?
The query could be more static but this means to make different prepared statements and execute it depending of $_POST checks.
It depends on many variables because this query show results in a table that contains search fields and column to order.
A full example of query would be like this (depending of $_POST checks):
SELECT id, title, content, priority, date, delivery FROM tasks INNER JOIN op ON task.op = op.opId WHERE op IN (4851,8965,78562) AND title LIKE '%PHT%' AND content LIKE '%%' AND priority = '2' ORDER BY date DESC LIMIT 0, 10
An excellent question. And thank you for moving to prepared statements. It seems that after all those years of struggle, the idea finally is starting to take over.
Disclaimer: there will be links to my own site because I am helping people with PHP for 20+ years and got an obsession with writing articles about most common issues.
Yes, it's perfectly possible. Check out my article, How to create a search filter for mysqli for the fully functional example.
For the WHERE part, all you need is to create two separate arrays - one containing query conditions with placeholders and one containing actual values for these placeholders, i.e:
WHERE clause
$conditions = [];
$parameters = [];
if (!empty($_POST["content"])) {
$conditions[] = 'content LIKE ?';
$parameters[] = '%'.$_POST['content ']."%";
}
and so on, for all search conditions.
Then you could implode all the conditions using AND string as a glue, and get a first-class WHERE clause:
if ($conditions)
{
$where .= " WHERE ".implode(" AND ", $conditions);
}
The routine is the same for all search conditions, but it will be a bit different for the IN() clause.
IN() clause
is a bit different as you will need more placeholders and more values to be added:
if (!empty($_POST["opID"])) {
$in = str_repeat('?,', count($array) - 1) . '?';
$conditions[] = "opID IN ($in)";
$parameters = array_merge($parameters, $_POST["opID"]);
}
this code will add as many ? placeholders to the IN() clause as many elements in the $_POST["opID"] and will add all those values to the $parameters array. The explanation can be found in the adjacent article in the same section on my site.
After you are done with WHERE clause, you can move to the rest of your query
ORDER BY clause
You cannot parameterize the order by clause, because field names and SQL keywords cannot be represented by a placeholder. And to tackle with this problem I beg you to use a whitelisting function I wrote for this exact purpose. With it you can make your ORDER BY clause 100% safe but perfectly flexible. All you need is to predefine an array with field names allowed in the order by clause:
$sortColumns = ["title","content","priority"]; // add your own
and then get safe values using this handy function:
$orderField = white_list($_POST["column"], $sortColumns, "Invalid column name");
$order = white_list($_POST["order"], ["ASC","DESC"], "Invalid ORDER BY direction");
this is a smart function, that covers three different scenarios
in case no values were provided (i.e. $_POST["column"] is empty) the first value from the white list will be used, so it serves as a default value
in case a correct value provided, it will be used in the query
in case an incorrect value is provided, then an error will be thrown.
LIMIT clause
LIMIT values are perfectly parameterized so you can just add them to the $parameters array:
$limit = "LIMIT ?, ?";
$parameters[] = $offset;
$parameters[] = $recordsPerPage;
The final assembly
In the end, your query will be something like this
$sql = "SELECT id, title, content, priority, date, delivery
FROM tasks INNER JOIN ... $where ORDER BY `$orderField` $order $limit";
And it can be executed using the following code
$stmt = $mysqli->prepare($sql);
$stmt->bind_param(str_repeat("s", count($parameters)), ...$parameters);
$stmt->execute();
$data = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
where $data is a conventional array contains all the rows returned by the query.

PHP security concerns around in clause that is a concatenated string

Given the following code
<?php
$values = array('Foo' =>'Foo' ,'Bar' =>'Bar' );
$separated = "'" . implode("','", $values)."'";
$sql = 'SELECT NAME,AGE FROM CATS WHERE TITLE IN(' .$separated.')' ;
print_r($sql);
produces:
SELECT NAME,AGE FROM CATS WHERE TITLE IN('Foo','Bar')
Is there anything I need to be aware of about SQL injection using this type of query builder? If so, what is an attack that can occur?
The only rule of SQL security:
NO value should be added to a query directly, but via placeholder only
So, you have to use a library that supports placeholders.
Assuming your database is mysql, the best choice would be safemysql, which will let you have as simple code as this:
$sql = 'SELECT NAME,AGE FROM CATS WHERE TITLE IN(?a)';
$data = $db->getArr($sql, $values);
print_r($data);
or you can use PDO, but it will take you a lot more trouble
You should never use any variables in queries no matter where they come from. A solution for PDO and parameterized queries will be to add placeholders to the query.
I do it something like this:
function getPlaceholders ($array) {
return !empty($array)
? implode(',', array_fill(0, count($array), '?'))
: null;
}
$userIds = array(1,2,3,4);
$sql = 'SELECT FROM users WHERE id IN (' . $this->getPlaceholders($userIds) . ')';
$result = pdo_query($sql, $userIds);
Normally you would have this in a OOP-format.
$userIds = array(1,2,3,4);
$sql = 'SELECT FROM users WHERE id IN (' . $this->getPlaceholders($userIds) . ')';
$result = $this->db->query($sql, $userIds);
// common file which is extended
public function getPlaceholders ($array) {
return !empty($array)
? implode(',', array_fill(0, count($array), '?'))
: null;
}
This will generate a query like:
SELECT FROM users WHERE id IN (?,?,?,?)

Create SQL statement in PHP function using an Array

I am creating a PHP function that will take some values, one of which is an array, that I need to use in a MySQL query.
I am creating the array as follows:
$newsArray = createArticleArray(array(2,20,3),5);
Then the function looks something like this (cut down for readability)
function createArticleArray($sectionArray = array(1),$itemsToShow)
{
$SQL = "
SELECT
*
FROM
tbl_section
WHERE
(tbl_section.fld_section_uid = 2 OR tbl_section.fld_section_uid = 20 OR tbl_section.fld_section_uid = 3)
ORDER BY
tbl_article.fld_date_created DESC LIMIT 0,$itemsToShow";
}
The section tbl_section.fld_section_uid = 2 OR tbl_section.fld_section_uid = 20 OR tbl_section.fld_section_uid = 3 is where I need to use the array values.
Basically I need to loop through the values in the array making up that part of the query, however I am having a little problem on how to show or not show the "OR" bits of it as there might be only 1 value or as many as I need.
I was thinking of something like this:
foreach($sectionArray as $section)
{
$sqlString = $sqlString . "tbl_section.fld_section_uid = $section OR";
}
but I don't know how to work out if to put the "OR" in there.
Use implode.
$conditionParts = array();
foreach($sectionArray as $section){
$conditionParts[] = "tbl_section.fld_section_uid = $section";
}
$sqlString .= implode(' OR ', $conditionParts);
This solution answers your question and show you how to use the implode function, but for your specific case you should really use the IN operator.
$sqlString .= "tbl_section.fld_section_uid IN(".implode(',', $sectionArray).")";
One solution is to put an extraneous 0 at the end to consume the final "OR" without any effect. The query parser will just remove it: A OR B OR C OR 0 is turned into A OR B OR C.
Another solution is to use implode to insert the OR:
$sqlString = "tbl_section.fld_section = "
. implode($sectionArray," OR tbl_section.fld_section_uid = ");
Of course, the correct solution is just to use IN:
"WHERE tbl_section.fld_section_uid IN(".implode($sectionArray,',').")";
The query can be made simpler and easier to generate if you use WHERE <column> IN (value1,value2,...) syntax.
Use PHP's implode to produce the (value1,value2,...) part:
$SQL .= ' WHERE tbl_section.fld_section_uid IN (' . implode(',', $array) . ') ';
Yields something like this:
SELECT
...
WHERE tbl_section.fld_section_uid IN (2,20,3)
...
Use PDO's prepare method: http://uk3.php.net/manual/en/pdo.prepare.php
$statement = $pdo->prepare("
SELECT
*
FROM
tbl_section
WHERE
(tbl_section.fld_section_uid = ? OR tbl_section.fld_section_uid = ? OR tbl_section.fld_section_uid = ?)
ORDER BY
tbl_article.fld_date_created DESC LIMIT 0,$itemsToShow");
$statement->execute( $sectionArray );
function createArticleArray($sectionArray = array(), $itemsToShow) {
$conditions = array();
for ($i = 0, $s = count($sectionArray); $i < $s; ++$i) {
$conditions[] = 'tbl_section.fld_section_uid = ' . (int) $sectionArray[$i];
}
$SQL = 'SELECT * FROM tbl_section WHERE ' . implode(' OR ', $conditions) . ' ORDER BY tbl_article.fld_date_created DESC LIMIT 0, ' . (int) $itemsToShow;
}

Categories