site_oueb_2/DBSR-master/src/DBSR.php

1094 lines
44 KiB
PHP
Executable File

<?php
/**
* DBSR provides functionality for commiting search-and-replace-operations on MySQL databases.
*/
class DBSR
{
// Constants
/**
* Version string indicating the DBSR version.
* @var string
*/
const VERSION = '2.2.0';
/**
* Option: use case-insensitive search and replace.
* @var boolean
*/
const OPTION_CASE_INSENSITIVE = 0;
/**
* Option: process *all* database rows.
* @var boolean
*/
const OPTION_EXTENSIVE_SEARCH = 1;
/**
* Option: number of rows to process simultaneously.
* @var integer
*/
const OPTION_SEARCH_PAGE_SIZE = 2;
/**
* Option: use strict matching.
* @var boolean
*/
const OPTION_VAR_MATCH_STRICT = 3;
/**
* Option: up to how many decimals floats should be matched.
* @var integer
*/
const OPTION_FLOATS_PRECISION = 4;
/**
* Option: automatically convert character sets.
* @var boolean
*/
const OPTION_CONVERT_CHARSETS = 5;
/**
* Option: cast all replace-values to the original type.
* @var boolean
*/
const OPTION_VAR_CAST_REPLACE = 6;
/**
* Option: write changed values back to the database.
* @var boolean
*/
const OPTION_DB_WRITE_CHANGES = 7;
/**
* Option: interpret serialized strings as PHP types.
* @var boolean
*/
const OPTION_HANDLE_SERIALIZE = 8;
/**
* Option: reverses the filters causing to search *only* in mentioned tables/columns.
* @var array
*/
const OPTION_REVERSED_FILTERS = 9;
/**
* Option: lock tables when running.
* @var boolean
*/
const OPTION_LOCK_TABLES = 10;
// Static methods
/**
* Creates a new class with the given name if it does not exists.
*
* @param string $className The name of the class.
*/
public static function createClass($className)
{
if (!class_exists($className, false)) {
$classArray = explode('\\', $className);
if (count($classArray) > 1) {
$className = array_pop($classArray);
$namespace = implode('\\', $classArray);
eval('namespace ' . $namespace . ' { class ' . $className . ' {} }');
} else {
eval('class ' . $className . ' {}');
}
}
}
/**
* Returns the PHP type for any MySQL type according to the PHP's settype() documentation.
* Will return 'string' for unknown / invalidly formatted types.
*
* @see http://php.net/manual/en/function.settype.php
*
* @param string $mysql_type The MySQL type.
*
* @return string The corresponding PHP type.
*/
public static function getPHPType($mysql_type)
{
// MySQL type regexes and corresponding PHP type
$types = array(
// Boolean types
'/^\s*BOOL(EAN)?\s*$/i' => 'boolean',
// Integer types
'/^\s*TINYINT\s*(?:\(\s*\d+\s*\)\s*)?$/i' => 'integer',
'/^\s*SMALLINT\s*(?:\(\s*\d+\s*\)\s*)?$/i' => 'integer',
'/^\s*MEDIUMINT\s*(?:\(\s*\d+\s*\)\s*)?$/i' => 'integer',
'/^\s*INT(EGER)?\s*(?:\(\s*\d+\s*\)\s*)?$/i' => 'integer',
'/^\s*BIGINT\s*(?:\(\s*\d+\s*\)\s*)?$/i' => 'integer',
// Float types
'/^\s*FLOAT\s*(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\)\s*)?$/i' => 'float',
'/^\s*DOUBLE(\s+PRECISION)?\s*(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\)\s*)?$/i' => 'float',
'/^\s*REAL\s*(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\)\s*)?$/i' => 'float',
'/^\s*DEC(IMAL)?\s*(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\)\s*)?$/i' => 'float',
'/^\s*NUMERIC\s*(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\)\s*)?$/i' => 'float',
'/^\s*FIXED\s*(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\)\s*)?$/i' => 'float',
);
// Try each type
foreach ($types as $regex => $type) {
// Test on a whitespace-free version
if (preg_match($regex, $mysql_type)) {
return $type;
}
}
// If nothing matches, return default (string)
return 'string';
}
// Properties
/**
* The PDO instance used for connecting to the database.
* @var PDO
*/
protected $pdo;
/**
* The default charset used by the database connection.
* @var string
*/
private $_pdo_charset;
/**
* The default collation used by the database connection.
* @var string
*/
private $_pdo_collation;
/**
* The callback used by DBRunner.
* @var callback
*/
private $_dbr_callback;
/**
* All options of the current instance.
* @var array
*/
protected $options = array(
self::OPTION_CASE_INSENSITIVE => false,
self::OPTION_EXTENSIVE_SEARCH => false,
self::OPTION_SEARCH_PAGE_SIZE => 10000,
self::OPTION_VAR_MATCH_STRICT => true,
self::OPTION_FLOATS_PRECISION => 5,
self::OPTION_CONVERT_CHARSETS => true,
self::OPTION_VAR_CAST_REPLACE => true,
self::OPTION_DB_WRITE_CHANGES => true,
self::OPTION_HANDLE_SERIALIZE => true,
self::OPTION_REVERSED_FILTERS => false,
self::OPTION_LOCK_TABLES => true,
);
/**
* The filters for tables/columns.
* @var array
*/
protected $filters = array();
/**
* The search-values.
* @var array
*/
protected $search = array();
/**
* The replace-values.
* @var array
*/
protected $replace = array();
/**
* An array of search-values converted per charset.
* @var array
*/
protected $search_converted = array();
// Methods
/**
* Constructor: sets the PDO instance for use with this DBSR instance.
*
* @param PDO $pdo A PDO instance representing a connection to a MySQL database.
* @throws RuntimeException If the a required PHP extension is not available.
* @throws InvalidArgumentException If the given PDO instance does not represent a MySQL database.
*/
public function __construct(PDO $pdo)
{
// Check if the required PCRE library is available
if (!extension_loaded('pcre')) {
throw new RuntimeException('The pcre (Perl-compatible regular expressions) extension is required for DBSR to work!');
}
// Check if the PDO represents a connection to a MySQL database
if ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) != 'mysql') {
throw new InvalidArgumentException('The given PDO instance is not representing an MySQL database!');
}
// Save the PDO instance
$this->pdo = $pdo;
}
/**
* Returns the value of an DBSR option.
*
* @param integer $option One of the DBSR::OPTION_* constants.
*
* @return mixed The value of the requested option or NULL if unsuccessful.
*/
public function getOption($option)
{
return isset($this->options[$option]) ? $this->options[$option] : null;
}
/**
* Sets an option on this instance.
*
* @param integer $attribute The attribute to be set.
* @param mixed $value The new value for the given attribute.
*
* @throws PDOException If any database error occurs.
* @throws PDOException If any database error occurs.
*/
public function setOption($option, $value)
{
// Only set known options
if (!isset($this->options[$option])) {
return false;
}
switch ($option) {
case static::OPTION_SEARCH_PAGE_SIZE:
// Require the page size to be greater than 0
if (is_int($value) && $value > 0) {
$this->options[$option] = $value;
return true;
} else {
return false;
}
case static::OPTION_FLOATS_PRECISION:
// Require the precision to be greater than or equal to 0
if (is_int($value) && $value >= 0) {
$this->options[$option] = $value;
return true;
} else {
return false;
}
default:
// By default, check if the type is equal
if (gettype($this->options[$option]) == gettype($value)) {
// Allow setting the same type
$this->options[$option] = $value;
return true;
} else {
// Don't allow setting the wrong type
return false;
}
}
}
/**
* Sets the filters by which to filter tables/columns.
*
* @param array $filters The filters as an associative array. For example:
* array(
* 'entire_table',
* array(
* 'column',
* 'in',
* 'every',
* 'table',
* ),
* 'table' => 'specific_column',
* 'table' => array(
* 'specific',
* 'columns',
* ),
* )
*/
public function setFilters(array $filters)
{
// Array for the parsed filters
$filters_parsed = array();
// For each filter
foreach ($filters as $key => $value) {
if (is_int($key)) {
if (is_string($value)) {
// Entire table
$filters_parsed[$value] = true;
} elseif (is_array($value)) {
// Skip empty arrays
if (!count($value)) {
continue;
}
// Require strings
foreach ($value as $v) {
if (!is_string($v)) {
throw new InvalidArgumentException('Only strings qualify as column names!');
}
}
// Save it
if (isset($filters_parsed['.'])) {
$filters_parsed['.'] = array_values(array_unique(array_merge($filters_parsed['.'], array_values($value))));
} else {
$filters_parsed['.'] = array_values(array_unique($value));
}
} else {
throw new InvalidArgumentException('The filter array can only contain strings or arrays!');
}
} else {
if (is_string($value)) {
// Single column
if (isset($filters_parsed[$key])) {
$filters_parsed[$key] = array_values(array_unique(array_merge($filters_parsed[$key], array($value))));
} else {
$filters_parsed[$key] = array($value);
}
} elseif (is_array($value)) {
// Skip empty arrays
if (!count($value)) {
continue;
}
// Require strings
foreach ($value as $v) {
if (!is_string($v)) {
throw new InvalidArgumentException('Only strings qualify as column names!');
}
}
// Save it
if (isset($filters_parsed[$key])) {
$filters_parsed[$key] = array_values(array_unique(array_merge($filters_parsed[$key], array_values($value))));
} else {
$filters_parsed[$key] = array_values(array_unique($value));
}
} else {
throw new InvalidArgumentException('The filter array can only contain strings or arrays!');
}
}
}
// Save the parsed filters
$this->filters = $filters_parsed;
}
/**
* Resets all filters.
*/
public function resetFilters()
{
$this->filters = array();
}
/**
* Indicated whether the given table / column is filtered.
* @param string $table The name of the table.
* @param string $column (Optional.) Then name of the column.
*/
public function isFiltered($table, $column = null)
{
if ($this->getOption(static::OPTION_REVERSED_FILTERS)) {
// Reversed filters
if ($column == null) {
// Never filter reversed based on table only, since there may be non-table-specific columns in it
return false;
} else {
// Process columns if the entire table is filtered or if the column is filtered for either this table or in global
return !(
isset($this->filters[$table]) && $this->filters[$table] === true ||
isset($this->filters[$table]) && in_array($column, $this->filters[$table], true) ||
isset($this->filters['.']) && in_array($column, $this->filters['.'], true)
);
}
} else {
// Normal filters
if ($column == null) {
// Only skip tables if the entire table is filtered
return isset($this->filters[$table]) && $this->filters[$table] === true;
} else {
// Skip columns if the entire table is filtered or if the column is filtered for either this table or in global
return
isset($this->filters[$table]) && $this->filters[$table] === true ||
isset($this->filters[$table]) && in_array($column, $this->filters[$table], true) ||
isset($this->filters['.']) && in_array($column, $this->filters['.'], true)
;
}
}
}
/**
* Sets the search- and replace-values.
*
* @param array $search The values to search for.
* @param array $replace The values to replace with.
* @throws InvalidArgumentException If the search- or replace-values are invalid.
*/
public function setValues(array $search, array $replace)
{
// Check array lengths
if (count($search) == 0 || count($replace) == 0 || count($search) != count($replace)) {
throw new InvalidArgumentException('The number of search- and replace-values is invalid!');
}
// Clean indices
$search = array_values($search);
$replace = array_values($replace);
// Remove all identical values
for ($i = 0; $i < count($search); $i++) {
if ($search[$i] === $replace[$i]) {
array_splice($search, $i, 1);
array_splice($replace, $i, 1);
$i--;
}
}
// Check the length again
if (count($search) == 0) {
throw new InvalidArgumentException('All given search- and replace-values are identical!');
}
// Set the values
$this->search = $search;
$this->replace = $replace;
}
/**
* Runs a search- and replace-action on the database.
*
* @throws PDOException If any database error occurs.
* @throws UnexpectedValueException If an error occurs processing data retrieved from the database.
* @return integer The number of changed rows.
*/
public function exec()
{
// Remove the time limit
if (!ini_get('safe_mode') && ini_get('max_execution_time') != '0') {
set_time_limit(0);
}
// Call the DBRunner
return $this->DBRunner(array($this, 'searchReplace'));
}
/**
* Runs through the database and execs the provided callback on every value.
*
* @param callable $callback The callback function to call on every value.
* @param array $search (Optional.) Search value to limit the matched rows to.
* @throws PDOException If any database error occurs.
* @throws UnexpectedValueException If an error occurs processing data retrieved from the database.
* @return integer The number of changed rows.
*/
protected function DBRunner($callback)
{
// Save the callback
$this->_dbr_callback = $callback;
// Count the number of changed rows
$result = 0;
// Set unserialize object handler
$unserialize_callback_func = ini_set('unserialize_callback_func', __CLASS__ . '::createClass');
// PDO attributes to set
$pdo_attributes = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
// Set PDO attributes and save the old values
foreach ($pdo_attributes as $attribute => $value) {
$pdo_attributes[$attribute] = $this->pdo->getAttribute($attribute);
$this->pdo->setAttribute($attribute, $value);
}
// Catch all Exceptions so that we can reset the errormode before rethrowing it
try {
// Figure out the connection character set and collation
$this->_pdo_charset = $this->pdo->query('SELECT @@character_set_client;', PDO::FETCH_COLUMN, 0)->fetch();
$this->_pdo_collation = $this->pdo->query('SELECT @@collation_connection;', PDO::FETCH_COLUMN, 0)->fetch();
// Get a list of all tables
$tables = $this->pdo->query('SHOW TABLES;', PDO::FETCH_COLUMN, 0)->fetchAll();
// If there are no tables, throw an error
if (count($tables) == 0) {
throw new Exception('Database does not contain any tables.');
}
if ($this->getOption(static::OPTION_LOCK_TABLES)) {
// Lock each table
$this->pdo->query('LOCK TABLES `' . implode('` WRITE, `', $tables) . '` WRITE;');
}
// Loop through all the (non-filtered) tables
foreach ($tables as $table) {
if (!$this->isFiltered($table)) {
$result += $this->_DBRTable($table, $callback);
}
}
} catch (Exception $e) {
// Since we support PHP 5.3 we cannot use finally, thus this block is empty and we continue below
}
if ($this->getOption(static::OPTION_LOCK_TABLES)) {
// Unlock all locked tables
$this->pdo->query('UNLOCK TABLES');
}
// Restore the old PDO attribute values
foreach ($pdo_attributes as $attribute => $value) {
$this->pdo->setAttribute($attribute, $value);
}
// Reset the unserialize object handler
ini_set('unserialize_callback_func', $unserialize_callback_func);
// Check whether an exception was thrown
if (isset($e) && $e instanceof Exception) {
// Rethrow the exception
throw $e;
} else {
// Return the results
return $result;
}
}
/**
* DBRunner: processes the given table.
*
* @param string $table Name of the table.
* @throws UnexpectedValueException If an error occurs processing data retrieved from the database.
* @return integer The number of changed rows.
*/
private function _DBRTable($table)
{
// List all columns of the current table
$columns_info = $this->pdo->query('SHOW FULL COLUMNS FROM `' . $table . '`;', PDO::FETCH_NAMED);
// Empty arrays for columns and keys
$columns = array();
$keys = array();
// Process each column
foreach ($columns_info as $column_info) {
// Determine type
$columns[$column_info['Field']] = array(
'null' => ($column_info['Null'] == 'YES'),
'type' => static::getPHPType($column_info['Type']),
'charset' => preg_replace('/^([a-z\d]+)_[\w\d]+$/i', '$1', $column_info['Collation']),
'collation' => $column_info['Collation'],
);
// Determine wheter it's part of a candidate key
$keys[$column_info['Key']][$column_info['Field']] = $columns[$column_info['Field']];
}
// Determine prefered candidate key
if (isset($keys['PRI'])) {
// Always prefere a primary key(set)
$keys = $keys['PRI'];
} elseif (isset($keys['UNI'])) {
// Though a unique key(set) also works
$keys = $keys['UNI'];
} else {
// If everything else fails, use the full column set
$keys = $columns;
}
// Filter columns
foreach ($columns as $column => $column_info) {
if ($this->isFiltered($table, $column)) {
unset($columns[$column]);
}
}
// Prepare a smart WHERE-statement
if (!$this->getOption(static::OPTION_EXTENSIVE_SEARCH)) {
$where = $this->_DBRWhereSearch($columns);
} else {
// No WHERE-statement
$where = '';
}
// Check if after filtering and WHERE-matching any valid columns are left
if (count($columns) == 0) {
return;
}
// Convert search-values to the correct charsets
if ($this->getOption(static::OPTION_CONVERT_CHARSETS)) {
foreach ($columns as $column => $column_info) {
if (!isset($this->search_converted[$column_info['charset']]) && $column_info['type'] == 'string' && !empty($column_info['charset']) && $column_info['charset'] != $this->_pdo_charset) {
foreach ($this->search as $i => $item) {
if (is_string($item)) {
$this->search_converted[$column_info['charset']][$i] = $this->pdo->query('SELECT CONVERT(_' . $this->_pdo_charset . $this->pdo->quote($item) . ' USING ' . $column_info['charset'] . ');', PDO::FETCH_COLUMN, 0)->fetch();
} else {
$this->search_converted[$column_info['charset']][$i] = $item;
}
}
}
}
}
// Get the number of rows
$row_count = (int) $this->pdo->query('SELECT COUNT(*) FROM `' . $table . '`' . $where . ';', PDO::FETCH_COLUMN, 0)->fetch();
// Count the number of changed rows
$row_change_count = 0;
// For each page
$page_size = $this->getOption(static::OPTION_SEARCH_PAGE_SIZE);
for ($page_start = 0; $page_start < $row_count; $page_start += $page_size) {
// Get the rows of this page
$rows = $this->pdo->query('SELECT * FROM `' . $table . '`' . $where . 'LIMIT ' . $page_start . ', ' . $page_size . ';', PDO::FETCH_ASSOC);
// Loop over each row
foreach ($rows as $row) {
if ($this->_DBRRow($table, $columns, $keys, $row) > 0) {
$row_change_count++;
}
}
}
// Return the number of changed rows
return $row_change_count;
}
/**
* DBRunner: processes the given row.
*
* @param string $table The name of the current table.
* @param array $columns The relevant columns of this table.
* @param array $keys The candidate keyset for this table.
* @param array $row The row to be processed.
* @throws UnexpectedValueException If an error occurs processing data retrieved from the database.
* @return integer The number of changed columns.
*/
private function _DBRRow($table, array $columns, array $keys, array $row)
{
// Array with row changes
$changeset = array();
// Convert columns
foreach ($columns + $keys as $column => $column_info) {
if (!settype($row[$column], $column_info['type'])) {
throw new UnexpectedValueException('Failed to convert `' . $table . '`.`' . $column . '` value to a ' . $column_info['type'] . ' for value "' . $row[$column] . '"!');
}
}
// Loop over each column
foreach ($columns as $column => $column_info) {
// Set the value
$value = &$row[$column];
// Call the callback
if ($this->getOption(static::OPTION_CONVERT_CHARSETS) && isset($this->search_converted[$column_info['charset']])) {
$value_new = call_user_func($this->_dbr_callback, $value, $this->search_converted[$column_info['charset']], $this->replace);
} else {
$value_new = call_user_func($this->_dbr_callback, $value);
}
// Check the result
if ($value_new !== $value) {
$changeset[$column] = $value_new;
}
}
// Update the row if nessecary
if (count($changeset) > 0 && $this->getOption(static::OPTION_DB_WRITE_CHANGES)) {
// Build the WHERE-statement for this row
$where = $this->_DBRWhereRow($keys, $row);
// Determine the updates
$updates = array();
foreach ($changeset as $column => $value_new) {
switch ($columns[$column]['type']) {
case 'integer':
$updates[] = '`' . $column . '` = ' . (int) $value_new;
break;
case 'float':
$updates[] = '`' . $column . '` = ' . (string) round((float) $value_new, $this->getOption(static::OPTION_FLOATS_PRECISION));
break;
case 'string':
default:
// First, escape the string and add quotes
$update_string = $this->pdo->quote((string) $value_new);
// Then, check the charset
if (!empty($columns[$column]['charset']) && $this->_pdo_charset != $columns[$column]['charset']) {
if ($this->getOption(static::OPTION_CONVERT_CHARSETS)) {
$update_string = 'CONVERT(_' . $this->_pdo_charset . $update_string . ' USING ' . $columns[$column]['charset'] . ')';
} else {
$update_string = 'BINARY ' . $update_string;
}
}
// Then, check the collation
if (!empty($columns[$column]['collation']) && $this->getOption(static::OPTION_CONVERT_CHARSETS) && $this->_pdo_collation != $columns[$column]['collation']) {
$update_string .= ' COLLATE ' . $columns[$column]['collation'];
}
// Finally, build and add the comparison for the WHERE-clause
$updates[] = '`' . $column . '` = ' . $update_string;
break;
}
}
// Commit the updates
$this->pdo->query('UPDATE `' . $table . '` SET ' . implode(', ', $updates) . $where . ';');
}
// Return the number of changed columns
return count($changeset);
}
/**
* DBRunner: constructs the WHERE-clause for searching.
*
* @param array $columns (Reference.) The columns to be searched. Inegible columns will be removed.
* @return mixed String with the constructed WHERE-clause, or FALSE if no column could be matched
* (thus the table may be skipped).
*/
private function _DBRWhereSearch(array &$columns)
{
// Array for WHERE-clause elements
$where = array();
// Loop over all columns
foreach ($columns as $column => $column_info) {
// By default there's no reason to include this column
$where_column = false;
// Loop over all search items
foreach ($this->search as $item) {
// If there's a valid WHERE-component, add it
if ($where_component = $this->_DBRWhereColumn($column, $column_info, $item, false)) {
$where[] = $where_component;
$where_column = true;
}
}
// Remove all columns which will never match since no valid WHERE-components could be constructed
if (!$where_column) {
unset($columns[$column]);
}
}
// Combine the WHERE-clause or empty it
if (count($where) > 0) {
return ' WHERE ' . implode(' OR ', $where) . ' ';
} else {
// Assert count($columns) == 0
if (count($columns) != 0) {
throw new LogicException('No WHERE-clause was constructed, yet there are valid columns left!');
}
// Since there are no valid columns left, we can skip processing this table
return false;
}
}
/**
* DBRunner: Constructs a WHERE-clause for the given row.
*
* @param array $keys The candidate keys to be used for constructing the WHERE-clause.
* @param array $row The row values.
* @return string The WHERE-clause for the given row.
*/
private function _DBRWhereRow(array $keys, array $row)
{
$where = array();
foreach ($keys as $key => $key_info) {
$where[] = $this->_DBRWhereColumn($key, $key_info, $row[$key], true);
}
return ' WHERE ' . implode(' AND ', $where) . ' ';
}
/**
* DBRunner: Constructs a WHERE component for the given column and value.
*
* @param string $column The column name.
* @param array $column_info Array with column info.
* @param mixed $value The value to match.
* @param boolean $string_exact Whether to use 'LIKE %value%'-style matching.
*
* @return mixed The WHERE component for the given parameters as a string,
* or FALSE if the value is not valid for the given column.
*/
private function _DBRWhereColumn($column, array $column_info, $value, $string_exact)
{
switch ($column_info['type']) {
case 'integer':
// Search for integer value
if (!$this->getOption(static::OPTION_VAR_MATCH_STRICT) || is_int($value)) {
// Add a where clause for the integer value
return '`' . $column . '` = ' . (int) $value;
}
break;
case 'float':
// Search for float difference (since floats aren't precise enough to compare directly)
if (!$this->getOption(static::OPTION_VAR_MATCH_STRICT) || is_float($value)) {
return 'ABS(`' . $column . '` - ' . (float) $value . ') < POW(1, -' . $this->getOption(static::OPTION_FLOATS_PRECISION) . ')';
}
break;
case 'string':
default:
// String search is even harder given the many possibly charsets
// If the search item is a float, we have to limit it to the maximum precision first
if (is_float($value)) {
$value = round($value, $this->getOption(static::OPTION_FLOATS_PRECISION));
}
if (!$string_exact) {
$value = '%' . (string) $value . '%';
}
// First, escape the string and add quotes
$where_string = $this->pdo->quote((string) $value);
// Then, check the charset
if (!empty($column_info['charset']) && $this->_pdo_charset != $column_info['charset']) {
if ($this->getOption(static::OPTION_CONVERT_CHARSETS)) {
$where_string = 'CONVERT(_' . $this->_pdo_charset . $where_string . ' USING ' . $column_info['charset'] . ')';
} else {
$where_string = 'BINARY ' . $where_string;
}
}
// Then, check the collation
if (!empty($column_info['collation']) && $this->getOption(static::OPTION_CONVERT_CHARSETS) && $this->_pdo_collation != $column_info['collation']) {
if ($this->getOption(static::OPTION_CASE_INSENSITIVE)) {
$where_string .= ' COLLATE ' . preg_replace('/_cs$/i', '_ci', $column_info['collation']);
} else {
$where_string .= ' COLLATE ' . $column_info['collation'];
}
}
// Column name
$column = '`' . $column . '`';
// Case insensitivity
if (!empty($column_info['collation']) && $this->getOption(static::OPTION_CASE_INSENSITIVE) && preg_replace('/^.*_([a-z]+)$/i', '$1', $column_info['collation']) == 'cs') {
$column .= ' COLLATE ' . preg_replace('/_cs$/i', '_ci', $column_info['collation']);
}
// Add the column
$where_string = $column . ' ' . ($string_exact ? '=' : 'LIKE') . ' ' . $where_string;
if (!empty($column_info['charset']) && !$this->getOption(static::OPTION_CONVERT_CHARSETS) && $this->_pdo_charset != $column_info['charset']) {
$where_string = 'BINARY ' . $where_string;
}
// Finally, build and add the comparison for the WHERE-clause
return $where_string;
}
// It seems the value was not valid for this column
return false;
}
/**
* Runs a search-and-replace action on the provided value.
*
* @var mixed $value The value to search through.
* @return mixed The value with all occurences of search items replaced.
*/
protected function searchReplace($value)
{
// The new value
$new_value = $value;
// For each type
switch (true) {
case is_array($value):
// The result is also an array
$new_value = array();
// Loop through all the values
foreach ($value as $key => $element) {
$new_value[$this->searchReplace($key)] = $this->searchReplace($element);
}
break;
case is_bool($value):
for ($i = 0; $i < count($this->search); $i++) {
if ($new_value === $this->search[$i] || !$this->getOption(static::OPTION_VAR_MATCH_STRICT) && $new_value == $this->search[$i]) {
$new_value = $this->replace[$i];
}
}
break;
case is_float($value):
$float_precision = pow(10, -1 * $this->getOption(static::OPTION_FLOATS_PRECISION));
for ($i = 0; $i < count($this->search); $i++) {
if (is_float($this->search[$i]) && abs($new_value - $this->search[$i]) < $float_precision ||
!$this->getOption(static::OPTION_VAR_MATCH_STRICT) && (
$new_value == $this->search[$i] ||
abs($new_value - (float) $this->search[$i]) < $float_precision
)
) {
$new_value = $this->replace[$i];
}
}
break;
case is_int($value):
for ($i = 0; $i < count($this->search); $i++) {
if ($new_value === $this->search[$i] || !$this->getOption(static::OPTION_VAR_MATCH_STRICT) && $new_value == $this->search[$i]) {
$new_value = $this->replace[$i];
}
}
break;
case is_object($value):
// Abuse the fact that corrupted serialized strings are handled by our own regexes
$new_value = unserialize($this->searchReplace(preg_replace('/^O:\\d+:/', 'O:0:', serialize($new_value))));
break;
case is_string($value):
// Regex for detecting serialized strings
$serialized_regex = '/^(?:a:\\d+:\\{.*\\}|b:[01];|d:\\d+\\.\\d+;|i:\\d+;|N;|O:\\d+:"[a-zA-Z_\\x7F-\\xFF][a-zA-Z0-9_\\x7F-\\xFF]*(?:\\\\[a-zA-Z_\\x7F-\\xFF][a-zA-Z0-9_\\x7F-\\xFF]*)*":\\d+:\\{.*\\}|s:\\d+:".*";)$/Ss';
// Try unserializing it
$unserialized = @unserialize($new_value);
// Check if if actually was unserialized
if ($this->getOption(static::OPTION_HANDLE_SERIALIZE) && ($unserialized !== false || $new_value === serialize(false)) && !is_object($unserialized)) {
// Process recursively
$new_value = serialize($this->searchReplace($unserialized));
} elseif ($this->getOption(static::OPTION_HANDLE_SERIALIZE) && (is_object($unserialized) || preg_match($serialized_regex, $new_value))) {
// If it looks like it's serialized, use special regexes for search-and-replace
// TODO: split arrays/objects and process recursively?
// Search and replace booleans
if ($changed_value = preg_replace_callback('/b:([01]);/S', array($this, '_searchReplace_preg_callback_boolean'), $new_value)) {
$new_value = $changed_value;
}
// Search and replace integers
if ($changed_value = preg_replace_callback('/i:(\\d+);/S', array($this, '_searchReplace_preg_callback_integer'), $new_value)) {
$new_value = $changed_value;
}
// Search and replace floats
if ($changed_value = preg_replace_callback('/d:(\\d+)\.(\\d+);/S', array($this, '_searchReplace_preg_callback_float'), $new_value)) {
$new_value = $changed_value;
}
// Search-and-replace object names (and update length)
if ($changed_value = preg_replace_callback('/O:\\d+:"([a-zA-Z_\\x7F-\\xFF][a-zA-Z0-9_\\x7F-\\xFF]*(?:\\\\[a-zA-Z_\\x7F-\\xFF][a-zA-Z0-9_\\x7F-\\xFF]*)*)":(\\d+):{(.*)}/Ss', array($this, '_searchReplace_preg_callback_objectname'), $new_value)) {
$new_value = $changed_value;
}
// Search-and-replace strings (and update length)
if ($changed_value = preg_replace_callback('/s:\\d+:"(.*?|a:\\d+:{.*}|b:[01];|d:\\d+\\.\\d+;|i:\d+;|N;|O:\\d+:"[a-zA-Z_\\x7F-\\xFF][a-zA-Z0-9_\\x7F-\\xFF]*":\\d+:{.*}|s:\\d+:".*";)";/Ss', array($this, '_searchReplace_preg_callback_string'), $new_value)) {
$new_value = $changed_value;
}
// If the regexes didn't change anything, run a normal replace just to be sure
if ($new_value == $value) {
for ($i = 0; $i < count($this->search); $i++) {
if (is_string($this->search[$i]) || !$this->getOption(static::OPTION_VAR_MATCH_STRICT)) {
$new_value = $this->getOption(static::OPTION_CASE_INSENSITIVE) ? str_ireplace((string) $this->search[$i], (string) $this->replace[$i], $new_value) : str_replace((string) $this->search[$i], (string) $this->replace[$i], $new_value);
}
}
}
} else {
for ($i = 0; $i < count($this->search); $i++) {
// Do a normal search-and-replace
if (is_string($this->search[$i]) || !$this->getOption(static::OPTION_VAR_MATCH_STRICT)) {
$new_value = $this->getOption(static::OPTION_CASE_INSENSITIVE) ? str_ireplace((string) $this->search[$i], (string) $this->replace[$i], $new_value) : str_replace((string) $this->search[$i], (string) $this->replace[$i], $new_value);
}
}
}
break;
}
// Return
return $new_value;
}
/**
* searchReplace: Callback for serialized boolean replacement.
* @param array $matches The matches corresponding to the boolean value as provided by preg_replace_callback.
* @return string The serialized representation of the result.
*/
private function _searchReplace_preg_callback_boolean($matches)
{
$result = $this->searchReplace((boolean) $matches[1]);
if (static::OPTION_VAR_CAST_REPLACE) {
$result = (boolean) $result;
}
return serialize($result);
}
/**
* searchReplace: Callback for serialized integer replacement.
* @param array $matches The matches corresponding to the integer value as provided by preg_replace_callback.
* @return string The serialized representation of the result.
*/
private function _searchReplace_preg_callback_integer($matches)
{
$result = $this->searchReplace((integer) $matches[1]);
if (static::OPTION_VAR_CAST_REPLACE) {
$result = (integer) $result;
}
return serialize($result);
}
/**
* searchReplace: Callback for serialized float replacement.
* @param array $matches The matches corresponding to the float value as provided by preg_replace_callback.
* @return string The serialized representation of the result.
*/
private function _searchReplace_preg_callback_float($matches)
{
$result = $this->searchReplace((float) ($matches[1] . '.' . $matches[2]));
if (static::OPTION_VAR_CAST_REPLACE) {
$result = (float) $result;
}
return serialize($result);
}
/**
* searchReplace: Callback for serialized object name replacement.
* @param array $matches The matches corresponding to the object name value as provided by preg_replace_callback.
* @return string The serialized representation of the result.
*/
private function _searchReplace_preg_callback_objectname($matches)
{
$name = preg_replace('/[^a-zA-Z0-9_\\x7F-\\xFF\\\\]+/', '', (string) $this->searchReplace($matches[1]));
return 'O:' . strlen($name) . ':"' . $name . '":' . $matches[2] . ':{' . $matches[3] . '}';
}
/**
* searchReplace: Callback for serialized string replacement.
* @param array $matches The matches corresponding to the string value as provided by preg_replace_callback.
* @return string The serialized representation of the result.
*/
private function _searchReplace_preg_callback_string($matches)
{
$result = $this->searchReplace($matches[1]);
if (static::OPTION_VAR_CAST_REPLACE) {
$result = (string) $result;
}
return serialize($result);
}
}