Вход Регистрация
Файл: system/lib/mysqli.class.php
Строк: 566
<?php
/**
 * @author col.shrapnel@gmail.com
 * @link http://phpfaq.ru/safemysql
 * 
 * Safe and convenient vay to handle SQL queries utilizing type-hinted placeholders.
 * 
 * Key features
 * - set of helper functions to get the desired result right out of query, like in PEAR::DB
 * - conditional query building using parse() method to build queries of whatever comlexity, 
 *   while keeping extra safety of placeholders
 * - type-hinted placeholders
 * 
 *  Type-hinted placeholders are great because 
 * - safe, as any other [properly implemented] placeholders
 * - no need for manual escaping or binding, makes the code extra DRY
 * - allows support for non-standard types such as identifier or array, which saves A LOT of pain in the back.
 * 
 * Supported placeholders at the moment are:
 * 
 * ?s ("string")  - strings (also DATE, FLOAT and DECIMAL)
 * ?i ("integer") - the name says it all 
 * ?n ("name")    - identifiers (table and field names) 
 * ?a ("array")   - complex placeholder for IN() operator  (substituted with string of 'a','b','c' format, without parentesis)
 * ?u ("update")  - complex placeholder for SET operator (substituted with string of `field`='value',`field`='value' format)
 * and
 * ?p ("parsed") - special type placeholder, for inserting already parsed statements without any processing, to avoid double parsing.
 * 
 * Some examples:
 *
 * $db = new SafeMySQL(); // with default settings
 * 
 * $opts = array(
 *        'user'    => 'user',
 *        'pass'    => 'pass',
 *        'db'      => 'db',
 *        'charset' => 'latin1'
 * );
 * $db = new SafeMySQL($opts); // with some of the default settings overwritten
 * 
 * 
 * $name = $db->getOne('SELECT name FROM table WHERE id = ?i',$_GET['id']);
 * $data = $db->getInd('id','SELECT * FROM ?n WHERE id IN ?a','table', array(1,2));
 * $data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit);
 *
 * $ids  = $db->getCol("SELECT id FROM tags WHERE tagname = ?s",$tag);
 * $data = $db->getAll("SELECT * FROM table WHERE category IN (?a)",$ids);
 * 
 * $data = array('offers_in' => $in, 'offers_out' => $out);
 * $sql  = "INSERT INTO stats SET pid=?i,dt=CURDATE(),?u ON DUPLICATE KEY UPDATE ?u";
 * $db->query($sql,$pid,$data,$data);
 * 
 * if ($var === NULL) {
 *     $sqlpart = "field is NULL";
 * } else {
 *     $sqlpart = $db->parse("field = ?s", $var);
 * }
 * $data = $db->getAll("SELECT * FROM table WHERE ?p", $bar, $sqlpart);
 * 
 */

class SafeMySQL
{

    private 
$conn;
    private 
$stats;
    private 
$emode;
    private 
$exname;

    private 
$defaults = array(
        
'host'      => 'localhost',
        
'user'      => 'root',
        
'pass'      => '',
        
'db'        => 'test',
        
'port'      => NULL,
        
'socket'    => NULL,
        
'pconnect'  => FALSE,
        
'charset'   => 'utf8',
        
'errmode'   => 'error'//or exception
        
'exception' => 'Exception'//Exception class name
    
);

    const 
RESULT_ASSOC MYSQLI_ASSOC;
    const 
RESULT_NUM   MYSQLI_NUM;

    function 
__construct($opt = array())
    {
        
$opt array_merge($this->defaults,$opt);

        
$this->emode  $opt['errmode'];
        
$this->exname $opt['exception'];

        if (
$opt['pconnect'])
        {
            
$opt['host'] = "p:".$opt['host'];
        }

        @
$this->conn mysqli_connect($opt['host'], $opt['user'], $opt['pass'], $opt['db'], $opt['port'], $opt['socket']);
        if ( !
$this->conn )
        {
            
$this->error(mysqli_connect_errno()." ".mysqli_connect_error());
        }

        
mysqli_set_charset($this->conn$opt['charset']) or $this->error(mysqli_error($this->conn));
        unset(
$opt); // I am paranoid
    
}

    
/**
     * Conventional function to run a query with placeholders. A mysqli_query wrapper with placeholders support
     * 
     * Examples:
     * $db->query("DELETE FROM table WHERE id=?i", $id);
     *
     * @param string $query - an SQL query with placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the query
     * @return resource|FALSE whatever mysqli_query returns
     */
    
public function query()
    {    
        return 
$this->rawQuery($this->prepareQuery(func_get_args()));
    }

    
/**
     * Conventional function to fetch single row. 
     * 
     * @param resource $result - myqli result
     * @param int $mode - optional fetch mode, RESULT_ASSOC|RESULT_NUM, default RESULT_ASSOC
     * @return array|FALSE whatever mysqli_fetch_array returns
     */
    
public function fetch($result,$mode=self::RESULT_ASSOC)
    {
        return 
mysqli_fetch_array($result$mode);
    }

    
/**
     * Conventional function to get number of affected rows. 
     * 
     * @return int whatever mysqli_affected_rows returns
     */
    
public function affectedRows()
    {
        return 
mysqli_affected_rows ($this->conn);
    }

    
/**
     * Conventional function to get last insert id. 
     * 
     * @return int whatever mysqli_insert_id returns
     */
    
public function insertId()
    {
        return 
mysqli_insert_id($this->conn);
    }

    
/**
     * Conventional function to get number of rows in the resultset. 
     * 
     * @param resource $result - myqli result
     * @return int whatever mysqli_num_rows returns
     */
    
public function numRows($result)
    {
        return 
mysqli_num_rows($result);
    }

    
/**
     * Conventional function to free the resultset. 
     */
    
public function free($result)
    {
        
mysqli_free_result($result);
    }

    
/**
     * Helper function to get scalar value right out of query and optional arguments
     * 
     * Examples:
     * $name = $db->getOne("SELECT name FROM table WHERE id=1");
     * $name = $db->getOne("SELECT name FROM table WHERE id=?i", $id);
     *
     * @param string $query - an SQL query with placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the query
     * @return string|FALSE either first column of the first row of resultset or FALSE if none found
     */
    
public function getOne()
    {
        
$query $this->prepareQuery(func_get_args());
        if (
$res $this->rawQuery($query))
        {
            
$row $this->fetch($res);
            if (
is_array($row)) {
                return 
reset($row);
            }
            
$this->free($res);
        }
        return 
FALSE;
    }

    
/**
     * Helper function to get single row right out of query and optional arguments
     * 
     * Examples:
     * $data = $db->getRow("SELECT * FROM table WHERE id=1");
     * $data = $db->getOne("SELECT * FROM table WHERE id=?i", $id);
     *
     * @param string $query - an SQL query with placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the query
     * @return array|FALSE either associative array contains first row of resultset or FALSE if none found
     */
    
public function getRow()
    {
        
$query $this->prepareQuery(func_get_args());
        if (
$res $this->rawQuery($query)) {
            
$ret $this->fetch($res);
            
$this->free($res);
            return 
$ret;
        }
        return 
FALSE;
    }

    
/**
     * Helper function to get single column right out of query and optional arguments
     * 
     * Examples:
     * $ids = $db->getCol("SELECT id FROM table WHERE cat=1");
     * $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s", $tag);
     *
     * @param string $query - an SQL query with placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the query
     * @return array|FALSE either enumerated array of first fields of all rows of resultset or FALSE if none found
     */
    
public function getCol()
    {
        
$ret   = array();
        
$query $this->prepareQuery(func_get_args());
        if ( 
$res $this->rawQuery($query) )
        {
            while(
$row $this->fetch($res))
            {
                
$ret[] = reset($row);
            }
            
$this->free($res);
        }
        return 
$ret;
    }

    
/**
     * Helper function to get all the rows of resultset right out of query and optional arguments
     * 
     * Examples:
     * $data = $db->getAll("SELECT * FROM table");
     * $data = $db->getAll("SELECT * FROM table LIMIT ?i,?i", $start, $rows);
     *
     * @param string $query - an SQL query with placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the query
     * @return array enumerated 2d array contains the resultset. Empty if no rows found. 
     */
    
public function getAll()
    {
        
$ret   = array();
        
$query $this->prepareQuery(func_get_args());
        if ( 
$res $this->rawQuery($query) )
        {
            while(
$row $this->fetch($res))
            {
                
$ret[] = $row;
            }
            
$this->free($res);
        }
        return 
$ret;
    }

    
/**
     * Helper function to get all the rows of resultset into indexed array right out of query and optional arguments
     * 
     * Examples:
     * $data = $db->getInd("id", "SELECT * FROM table");
     * $data = $db->getInd("id", "SELECT * FROM table LIMIT ?i,?i", $start, $rows);
     *
     * @param string $index - name of the field which value is used to index resulting array
     * @param string $query - an SQL query with placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the query
     * @return array - associative 2d array contains the resultset. Empty if no rows found. 
     */
    
public function getInd()
    {
        
$args  func_get_args();
        
$index array_shift($args);
        
$query $this->prepareQuery($args);

        
$ret = array();
        if ( 
$res $this->rawQuery($query) )
        {
            while(
$row $this->fetch($res))
            {
                
$ret[$row[$index]] = $row;
            }
            
$this->free($res);
        }
        return 
$ret;
    }

    
/**
     * Helper function to get a dictionary-style array right out of query and optional arguments
     * 
     * Examples:
     * $data = $db->getIndCol("name", "SELECT name, id FROM cities");
     *
     * @param string $index - name of the field which value is used to index resulting array
     * @param string $query - an SQL query with placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the query
     * @return array - associative array contains key=value pairs out of resultset. Empty if no rows found. 
     */
    
public function getIndCol()
    {
        
$args  func_get_args();
        
$index array_shift($args);
        
$query $this->prepareQuery($args);

        
$ret = array();
        if ( 
$res $this->rawQuery($query) )
        {
            while(
$row $this->fetch($res))
            {
                
$key $row[$index];
                unset(
$row[$index]);
                
$ret[$key] = reset($row);
            }
            
$this->free($res);
        }
        return 
$ret;
    }

    
/**
     * Function to parse placeholders either in the full query or a query part
     * unlike native prepared statements, allows ANY query part to be parsed
     * 
     * useful for debug
     * and EXTREMELY useful for conditional query building
     * like adding various query parts using loops, conditions, etc.
     * already parsed parts have to be added via ?p placeholder
     * 
     * Examples:
     * $query = $db->parse("SELECT * FROM table WHERE foo=?s AND bar=?s", $foo, $bar);
     * echo $query;
     * 
     * if ($foo) {
     *     $qpart = $db->parse(" AND foo=?s", $foo);
     * }
     * $data = $db->getAll("SELECT * FROM table WHERE bar=?s ?p", $bar, $qpart);
     *
     * @param string $query - whatever expression contains placeholders
     * @param mixed  $arg,... unlimited number of arguments to match placeholders in the expression
     * @return string - initial expression with placeholders substituted with data. 
     */
    
public function parse()
    {
        return 
$this->prepareQuery(func_get_args());
    }

    
/**
     * function to implement whitelisting feature
     * sometimes we can't allow a non-validated user-supplied data to the query even through placeholder
     * especially if it comes down to SQL OPERATORS
     * 
     * Example:
     *
     * $order = $db->whiteList($_GET['order'], array('name','price'));
     * $dir   = $db->whiteList($_GET['dir'],   array('ASC','DESC'));
     * if (!$order || !dir) {
     *     throw new http404(); //non-expected values should cause 404 or similar response
     * }
     * $sql  = "SELECT * FROM table ORDER BY ?p ?p LIMIT ?i,?i"
     * $data = $db->getArr($sql, $order, $dir, $start, $per_page);
     * 
     * @param string $iinput   - field name to test
     * @param  array  $allowed - an array with allowed variants
     * @param  string $default - optional variable to set if no match found. Default to false.
     * @return string|FALSE    - either sanitized value or FALSE
     */
    
public function whiteList($input,$allowed,$default=FALSE)
    {
        
$found array_search($input,$allowed);
        return (
$found === FALSE) ? $default $allowed[$found];
    }

    
/**
     * function to filter out arrays, for the whitelisting purposes
     * useful to pass entire superglobal to the INSERT or UPDATE query
     * OUGHT to be used for this purpose, 
     * as there could be fields to which user should have no access to.
     * 
     * Example:
     * $allowed = array('title','url','body','rating','term','type');
     * $data    = $db->filterArray($_POST,$allowed);
     * $sql     = "INSERT INTO ?n SET ?u";
     * $db->query($sql,$table,$data);
     * 
     * @param  array $input   - source array
     * @param  array $allowed - an array with allowed field names
     * @return array filtered out source array
     */
    
public function filterArray($input,$allowed)
    {
        foreach(
array_keys($input) as $key )
        {
            if ( !
in_array($key,$allowed) )
            {
                unset(
$input[$key]);
            }
        }
        return 
$input;
    }

    
/**
     * Function to get last executed query. 
     * 
     * @return string|NULL either last executed query or NULL if were none
     */
    
public function lastQuery()
    {
        
$last end($this->stats);
        return 
$last['query'];
    }

    
/**
     * Function to get all query statistics. 
     * 
     * @return array contains all executed queries with timings and errors
     */
    
public function getStats()
    {
        return 
$this->stats;
    }

    
/**
     * private function which actually runs a query against Mysql server.
     * also logs some stats like profiling info and error message
     * 
     * @param string $query - a regular SQL query
     * @return mysqli result resource or FALSE on error
     */
    
private function rawQuery($query)
    {
        
$start microtime(TRUE);
        
$res   mysqli_query($this->conn$query);
        
$timer microtime(TRUE) - $start;

        
$this->stats[] = array(
            
'query' => $query,
            
'start' => $start,
            
'timer' => $timer,
        );
        if (!
$res)
        {
            
$error mysqli_error($this->conn);

            
end($this->stats);
            
$key key($this->stats);
            
$this->stats[$key]['error'] = $error;
            
$this->cutStats();

            
$this->error("$error. Full query: [$query]");
        }
        
$this->cutStats();
        return 
$res;
    }

    private function 
prepareQuery($args)
    {
        
$query '';
        
$raw   array_shift($args);
        
$array preg_split('~(?[nsiuap])~u',$raw,null,PREG_SPLIT_DELIM_CAPTURE);
        
$anum  count($args);
        
$pnum  floor(count($array) / 2);
        if ( 
$pnum != $anum )
        {
            
$this->error("Number of args ($anum) doesn't match number of placeholders ($pnum) in [$raw]");
        }

        foreach (
$array as $i => $part)
        {
            if ( (
$i 2) == )
            {
                
$query .= $part;
                continue;
            }

            
$value array_shift($args);
            switch (
$part)
            {
                case 
'?n':
                    
$part $this->escapeIdent($value);
                    break;
                case 
'?s':
                    
$part $this->escapeString($value);
                    break;
                case 
'?i':
                    
$part $this->escapeInt($value);
                    break;
                case 
'?a':
                    
$part $this->createIN($value);
                    break;
                case 
'?u':
                    
$part $this->createSET($value);
                    break;
                case 
'?p':
                    
$part $value;
                    break;
            }
            
$query .= $part;
        }
        return 
$query;
    }

    private function 
escapeInt($value)
    {
        if (
$value === NULL)
        {
            return 
'NULL';
        }
        if(!
is_numeric($value))
        {
            
$this->error("Integer (?i) placeholder expects numeric value, ".gettype($value)." given");
            return 
FALSE;
        }
        if (
is_float($value))
        {
            
$value number_format($value0'.'''); // may lose precision on big numbers
        

        return 
$value;
    }

    private function 
escapeString($value)
    {
        if (
$value === NULL)
        {
            return 
'NULL';
        }
        return    
"'".mysqli_real_escape_string($this->conn,$value)."'";
    }

    private function 
escapeIdent($value)
    {
        if (
$value)
        {
            return 
"`".str_replace("`","``",$value)."`";
        } else {
            
$this->error("Empty value for identifier (?n) placeholder");
        }
    }

    private function 
createIN($data)
    {
        if (!
is_array($data))
        {
            
$this->error("Value for IN (?a) placeholder should be array");
            return;
        }
        if (!
$data)
        {
            return 
'NULL';
        }
        
$query $comma '';
        foreach (
$data as $value)
        {
            
$query .= $comma.$this->escapeString($value);
            
$comma  ",";
        }
        return 
$query;
    }

    private function 
createSET($data)
    {
        if (!
is_array($data))
        {
            
$this->error("SET (?u) placeholder expects array, ".gettype($data)." given");
            return;
        }
        if (!
$data)
        {
            
$this->error("Empty array for SET (?u) placeholder");
            return;
        }
        
$query $comma '';
        foreach (
$data as $key => $value)
        {
            
$query .= $comma.$this->escapeIdent($key).'='.$this->escapeString($value);
            
$comma  ",";
        }
        return 
$query;
    }

    private function 
error($err)
    {
        
$err  __CLASS__.": ".$err;

        if ( 
$this->emode == 'error' )
        {
            
$err .= ". Error initiated in ".$this->caller().", thrown";
            
trigger_error($err,E_USER_ERROR);
        } else {
            throw new 
$this->exname($err);
        }
    }

    private function 
caller()
    {
        
$trace  debug_backtrace();
        
$caller '';
        foreach (
$trace as $t)
        {
            if ( isset(
$t['class']) && $t['class'] == __CLASS__ )
            {
                
$caller $t['file']." on line ".$t['line'];
            } else {
                break;
            }
        }
        return 
$caller;
    }

    
/**
     * On a long run we can eat up too much memory with mere statsistics
     * Let's keep it at reasonable size, leaving only last 100 entries.
     */
    
private function cutStats()
    {
        if ( 
count($this->stats) > 100 )
        {
            
reset($this->stats);
            
$first key($this->stats);
            unset(
$this->stats[$first]);
        }
    }
}
Онлайн: 2
Реклама