| Current File : /home/jvzmxxx/wiki/extensions/Flow/includes/Data/Storage/DbStorage.php |
<?php
namespace Flow\Data\Storage;
use Flow\Data\ObjectManager;
use Flow\Data\ObjectStorage;
use Flow\Data\Utils\RawSql;
use Flow\Model\UUID;
use Flow\DbFactory;
use Flow\Exception\DataModelException;
/**
* Base class for all ObjectStorage implementers
* which use a database as the backing store.
*
* Includes some utility methods for database management and
* SQL security.
*/
abstract class DbStorage implements ObjectStorage {
/**
* @var DbFactory
*/
protected $dbFactory;
/**
* The revision columns allowed to be updated
*
* @var string[]|true Allow of selective columns to allow, or true to allow
* everything
*/
protected $allowedUpdateColumns = true;
/**
* This is to prevent 'Update not allowed on xxx' error during moderation when
* * old cache is not purged and still holds obsolete deleted column
* * old cache is not purged and doesn't have the newly added column
*
* @var string[] Array of columns to ignore
*/
protected $obsoleteUpdateColumns = array();
/**
* @param DbFactory $dbFactory
*/
public function __construct( DbFactory $dbFactory ) {
$this->dbFactory = $dbFactory;
}
/**
* Runs preprocessSqlArray on each element of an array.
*
* @param array $outer The array to check
* @return array Preprocessed SQL array.
* @throws DataModelException
*/
protected function preprocessNestedSqlArray( array $outer ) {
foreach ( $outer as $i => $data ) {
if ( !is_array( $data ) ) {
throw new DataModelException( "Unexpected non-array in nested SQL array" );
}
$outer[$i] = $this->preprocessSqlArray( $data );
}
return $outer;
}
/**
* At the moment, does three things:
* 1. Finds UUID objects and returns their database representation.
* 2. Checks for unarmoured raw SQL and errors out if it exists.
* 3. Finds armoured raw SQL and expands it out.
*
* @param array $data Query conditions for DatabaseBase::select
* @return array query conditions escaped for use
* @throws DataModelException
*/
protected function preprocessSqlArray( array $data ) {
// Assuming that all databases have the same escaping settings.
$db = $this->dbFactory->getDB( DB_SLAVE );
$data = UUID::convertUUIDs( $data, 'binary' );
foreach( $data as $key => $value ) {
if ( $value instanceof RawSql ) {
$data[$key] = $value->getSql( $db );
} elseif ( is_numeric( $key ) ) {
throw new DataModelException( "Unescaped raw SQL found in " . __METHOD__, 'process-data' );
} elseif ( !preg_match( '/^[A-Za-z0-9\._]+$/', $key ) ) {
throw new DataModelException( "Dangerous SQL field name '$key' found in " . __METHOD__, 'process-data' );
}
}
return $data;
}
/**
* Internal security function which checks a row object
* (for inclusion as a condition or a row for insert/update)
* for any numeric keys (= raw SQL), or field names with
* potentially unsafe characters.
*
* @param array $row The row to check.
* @return boolean True if raw SQL is found
*/
protected function hasUnescapedSQL( array $row ) {
foreach( $row as $key => $value ) {
if ( $value instanceof RawSql ) {
// Specifically allowed SQL
continue;
}
if ( is_numeric( $key ) ) {
return true;
}
if ( ! preg_match( '/^' . $this->getFieldRegexFragment() . '$/', $key ) ) {
return true;
}
}
return false;
}
/**
* Returns a regular expression fragment suitable for matching a valid
* SQL field name, and hopefully no injection attacks
* @return string Regular expression fragment
*/
protected function getFieldRegexFragment() {
return '\s*[A-Za-z0-9\._]+\s*';
}
/**
* Internal security function to check an options array for
* SQL injection and other funkiness
* @todo Currently only supports LIMIT, OFFSET and ORDER BY
* @param array $options An options array passed to a query.
* @return boolean
*/
protected function validateOptions( $options ) {
static $validUnaryOptions = array(
'UNIQUE',
'EXPLAIN',
);
$fieldRegex = $this->getFieldRegexFragment();
foreach( $options as $key => $value ) {
if ( is_numeric( $key ) && in_array( strtoupper( $value ), $validUnaryOptions ) ) {
continue;
} elseif ( is_numeric( $key ) ) {
wfDebug( __METHOD__.": Unrecognised unary operator $value\n" );
return false;
}
if ( $key === 'LIMIT' ) {
// LIMIT is one or two integers, separated by a comma.
if ( ! preg_match ( '/^\d+(,\d+)?$/', $value ) ) {
wfDebug( __METHOD__.": Invalid LIMIT $value\n" );
return false;
}
} elseif ( $key === 'ORDER BY' ) {
// ORDER BY is a list of field names with ASC / DESC afterwards
if ( is_string( $value ) ) {
$value = explode( ',', $value );
}
$orderByRegex = "/^\s*$fieldRegex\s*(ASC|DESC)?\s*$/i";
foreach( $value as $orderByField ) {
if ( ! preg_match( $orderByRegex, $orderByField ) ) {
wfDebug( __METHOD__.": invalid ORDER BY field $orderByField\n" );
return false;
}
}
} elseif ( $key === 'OFFSET' ) {
// OFFSET is just an integer
if ( ! is_numeric( $value ) ) {
wfDebug( __METHOD__.": non-numeric offset $value\n" );
return false;
}
} elseif ( $key === 'GROUP BY' ) {
if ( ! preg_match( "/^{$fieldRegex}(,{$fieldRegex})+$/", $value ) ) {
wfDebug( __METHOD__.": invalid GROUP BY field\n" );
}
} else {
wfDebug( __METHOD__.": Unknown option $key\n" );
return false;
}
}
// Everything passes
return true;
}
/**
* {@inheritDoc}
*/
public function validate( array $row ) {
return true;
}
/**
* Calculates the DB updates to be performed to update data from $old to
* $new.
*
* @param array $old
* @param array $new
* @return array
* @throws DataModelException
*/
public function calcUpdates( array $old, array $new ) {
$changeSet = ObjectManager::calcUpdatesWithoutValidation( $old, $new );
foreach ( $this->obsoleteUpdateColumns as $val ) {
// Need to use array_key_exists to check null value
if ( array_key_exists( $val, $changeSet ) ) {
unset( $changeSet[$val] );
}
}
if ( is_array( $this->allowedUpdateColumns ) ) {
$extra = array_diff( array_keys( $changeSet ), $this->allowedUpdateColumns );
if ( $extra ) {
throw new DataModelException( 'Update not allowed on: ' . implode( ', ', $extra ), 'process-data' );
}
}
return $changeSet;
}
}