27

I have three values in a string like this:

$villes = '"paris","fes","rabat"';

When I feed it into a prepared statement like this:

$sql    = 'SELECT distinct telecopie FROM `comptage_fax` WHERE `ville` IN(%s)';
$query  = $wpdb->prepare($sql, $villes);

echo $query; shows:

SELECT distinct telecopie FROM `comptage_fax` WHERE `ville` IN('\"CHAPELLE VIVIERS \",\"LE MANS \",\"QUEND\"')

It is not writing the string as three separate values -- it is just one string with the double quotes escaped.

How can I properly implement a prepared statement in WordPress with multiple values?

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
mgraph
  • 15,238
  • 4
  • 41
  • 75

7 Answers7

58

Try this code:

// Create an array of the values to use in the list
$villes = array("paris", "fes", "rabat");    

// Generate the SQL statement.
// The number of %s items is based on the length of the $villes array
$sql = "
  SELECT DISTINCT telecopie
  FROM `comptage_fax`
  WHERE `ville` IN(".implode(', ', array_fill(0, count($villes), '%s')).")
";

// Call $wpdb->prepare passing the values of the array as separate arguments
$query = call_user_func_array(array($wpdb, 'prepare'), array_merge(array($sql), $villes));

echo $query;
Pikamander2
  • 7,332
  • 3
  • 48
  • 69
DaveRandom
  • 87,921
  • 11
  • 154
  • 174
  • 1
    Note that this is arguably an overly complicated approach, but it gives you the advantage of being able to vary the contents of the array and the code will adapt accordingly with no modification. So you could populate `$villes` with 30 towns instead of just 3 and it will still work. – DaveRandom May 17 '12 at 10:57
  • 1
    Might want to consider adding this to the codex or submitting a patch to core that'll allow you to use this method for an IN statement. – Nick Budden Mar 26 '13 at 08:48
  • How can I add an AND statement near to the WHERE area. WHERE `ville` IN(".implode(', ', array_fill(0, count($villes), '%s')).") AND `table_name.name` = '%s' – Faysal Haque Mar 12 '15 at 00:16
  • Thanks for your code it helps me what I need :) here is my solution: http://stackoverflow.com/a/29000133/1993427 – Faysal Haque Mar 12 '15 at 01:09
  • 1
    Now, you can use expansion to make the code a bit cleaner, if your array is the last item in the `$prepare`: `$wpdb->prepare($sql,...$ville);` – Aaron Butacov May 19 '17 at 19:08
  • PHP Notice: wpdb::prepare was called incorrectly. Unsupported value type (array). Please see Debugging in WordPress for more information. (This message was added in version 4.8.2.) in E:\XAMPP\htdocs\mr-assistant\wp-includes\functions.php on line 5167 – Shapon Pal Jul 20 '20 at 07:24
28

WordPress already has a function for this purpose, see esc_sql(). Here is the definition of this function:

Escapes data for use in a MySQL query. Usually you should prepare queries using wpdb::prepare(). Sometimes, spot-escaping is required or useful. One example is preparing an array for use in an IN clause.

You can use it like this:

$villes = ["paris", "fes", "rabat"];
$villes = array_map(function($v) {
    return "'" . esc_sql($v) . "'";
}, $villes);
$villes = implode(',', $villes);
$query = "SELECT distinct telecopie FROM `comptage_fax` WHERE `ville` IN (" . $villes . ")"
7

FUNCTION:

function escape_array($arr){
    global $wpdb;
    $escaped = array();
    foreach($arr as $k => $v){
        if(is_numeric($v))
            $escaped[] = $wpdb->prepare('%d', $v);
        else
            $escaped[] = $wpdb->prepare('%s', $v);
    }
    return implode(',', $escaped);
}

USAGE:

$arr = array('foo', 'bar', 1, 2, 'foo"bar', "bar'foo");

$query = "SELECT values
FROM table
WHERE column NOT IN (" . escape_array($arr) . ")";

echo $query;

RESULT:

SELECT values
FROM table
WHERE column NOT IN ('foo','bar',1,2,'foo\"bar','bar\'foo')

May or may not be more efficient, however it is reusable.

Greenzilla
  • 71
  • 1
  • 3
2

First is a modern set of a non-WordPress techniques using a mysqli prepared statement with an unknown number of values in an array. The second snippet will be the WordPress equivalent.

Let's assume that the indexed array of input data is untrusted and accessible from $_GET['villes']. A prepared statement is the modern standard and preferred by professional developers over old/untrusted escaping techniques. The snippet to follow will return rows that have one of the ville values specified in the array. If the array is not declared or is empty, it will return ALL rows in the database table.

Native PHP techniques:

$sql = "SELECT DISTINCT telecopie FROM comptage_fax";
if (!empty($_GET['villes'])) {
    $count = count($_GET['villes']);
    $commaDelimitedPlaceholders = implode(',', array_fill(0, $count, '?'));
    $stmt = $conn->prepare("$sql WHERE ville IN ($commaDelimitedPlaceholders)");
    $stmt->bind_param(str_repeat('s', $count), ...$_GET['villes']);
    $stmt->execute();
    $result = $stmt->get_result();
} else {
    $result = $conn->query($sql);
}

From this point, you can access the rows of distinct telecopie values (which is technically an iterable result set object) as if iterating an indexed array of associative arrays with a simple foreach().

foreach ($result as $row) {
    echo $row['telecopie'];
}

With WordPress's helper methods the syntax is simpler because the variable binding and query execution is handled by get_results():

$sql = "SELECT DISTINCT telecopie FROM comptage_fax";
if (!empty($_GET['ville']) {
    $commaDelimitedPlaceholders = implode(',', array_fill(0, count($_GET['ville']), '%s'));
    $sql = $wpdb->prepare("$sql WHERE ville IN ($commaDelimitedPlaceholders)", $_GET['ville']);
}
$result = $wpdb->get_results($sql, ARRAY_A);

From this point, $result is an indexed array of associative arrays -- specifically because of ARRAY_A. $result is not a result set object like in the first native php snippet. This means that you can use both classic looping language constructor or the full suite of array_ functions on the data.

Useful References:

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
2

Here is my approach for sanitizing IN (...) values for $wpdb.

  1. I use a helper function that passes each value of the list through $wpdb->prepare() to ensure that it's properly escaped.
  2. The prepared value-list is inserted into the SQL query via sprintf().

The helper function:

// Helper function that returns a fully sanitized value list.
function _prepare_in ( $values ) {
    return implode( ',', array_map( function ( $value ) {
        global $wpdb;

        // Use the official prepare() function to sanitize the value.
        return $wpdb->prepare( '%s', $value );
    }, $values ) );
};

Sample usage:

// Sample 1 - note that we use "sprintf()" to build the SQL query!
$status_cond = sprintf(
    'post_status IN (%s)',
    _prepare_in( $status )
);
$posts = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE $status_cond;" );


// Sample 2:
$posts = $wpdb->get_col( sprintf( "
    SELECT ID
    FROM $wpdb->posts
    WHERE post_status IN (%s) AND post_type IN (%s)
    ;",
    _prepare_in( $status ),
    _prepare_in( $post_types )
) );
Philipp
  • 10,240
  • 8
  • 59
  • 71
2

I created a tiny function that will generate the placeholders for a prepared statement from an array, i.e., something like (%s,%d,%f), and conveniently, it will know exactly which placeholder to use depending on each item of the array.

function generate_wpdb_prepare_placeholders_from_array( $array ) {
    $placeholders = array_map( function ( $item ) {
        return is_string( $item ) ? '%s' : ( is_float( $item ) ? '%f' : ( is_int( $item ) ? '%d' : '' ) );
    }, $array );
    return '(' . join( ',', $placeholders ) . ')';
}

Considering the example from the question, first you'd need to convert the values to an array:

$villes = array( "paris", "fes", "rabat" );   
$in_str = generate_wpdb_prepare_placeholders_from_array( $villes );
$sql    = "SELECT distinct telecopie FROM `comptage_fax` WHERE `ville` IN {$in_str}";
$query  = $wpdb->prepare( $sql, $villes );
Pablo S G Pacheco
  • 2,550
  • 28
  • 28
-3

The prepare function also takes an array as the second parameter.

You can try converting $villes like this:

Current

<?php
$villes = '"paris","fes","rabat"';
?

Change it to

<?php
$villes = array("paris","fes","rabat");
?>

Now, try passing $villes to the prepare func and see if it works. Hope it helps.

web-nomad
  • 6,003
  • 3
  • 34
  • 49