Skip to content

Commit

Permalink
Implemented identifier quoting
Browse files Browse the repository at this point in the history
  • Loading branch information
lorenzo committed Oct 14, 2012
1 parent 17df49a commit acd33cb
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 6 deletions.
17 changes: 11 additions & 6 deletions lib/Cake/Model/Datasource/Database/Connection.php
Expand Up @@ -73,24 +73,29 @@ public function __construct($config) {
throw new MissingDriverException(array('driver' => $config['datasource']));
}

$this->setDriver($config['datasource']);
$this->driver($config['datasource']);
if (!$this->_driver->enabled()) {
throw new MissingExtensionException(array('driver' => get_class($this->_driver)));
}
}

/**
* Sets the driver instance. If an string is passed it will be treated
* as a class name and will be instantiated
* as a class name and will be instantiated.
*
* If no params are passed it will return the current driver instance
*
* @param string|Driver $driver
* @return void
* @return Driver
**/
public function setDriver($driver) {
public function driver($driver = null) {
if ($driver === null) {
return $this->_driver;
}
if (is_string($driver)) {
$driver = new $driver;
}
$this->_driver = $driver;
return $this->_driver = $driver;
}

/**
Expand Down Expand Up @@ -393,7 +398,7 @@ public function quote($value, $type = null) {
* @return string
**/
public function quoteIdentifier($identifier) {

return $this->_driver->quoteIdentifier($identifier);
}

/**
Expand Down
58 changes: 58 additions & 0 deletions lib/Cake/Model/Datasource/Database/Driver.php
Expand Up @@ -9,6 +9,20 @@
**/
abstract class Driver {

/**
* String used to start a database identifier quoting to make it safe
*
* @var string
**/
public $startQuote = '"';

/**
* String used to end a database identifier quoting to make it safe
*
* @var string
**/
public $endQuote = '"';

/**
* Establishes a connection to the database server
*
Expand Down Expand Up @@ -106,4 +120,48 @@ public function rollbackSavePointSQL($name) {
* @return string
**/
public abstract function quote($value, $type);

/**
* Quotes a database identifier (a column name, table name, etc..) to
* be used safely in queries without the risk of using reserver words
*
* @param string $identifier
* @return string
**/
public function quoteIdentifier($identifier) {
$identifier = trim($identifier);

if ($identifier === '*') {
return '*';
}

if (preg_match('/^[\w-]+(?:\.[^ \*]*)*$/', $identifier)) { // string, string.string
if (strpos($identifier, '.') === false) { // string
return $this->startQuote . $identifier . $this->endQuote;
}
$items = explode('.', $identifier);
return $this->startQuote . implode($this->endQuote . '.' . $this->startQuote, $items) . $this->endQuote;
}

if (preg_match('/^[\w-]+\.\*$/', $identifier)) { // string.*
return $this->startQuote . str_replace('.*', $this->endQuote . '.*', $identifier);
}

if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) { // Functions
return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')';
}

if (preg_match('/^([\w-]+(\.[\w-]+|\(.*\))*)\s+AS\s*([\w-]+)$/i', $identifier, $matches)) {
return preg_replace(
'/\s{2,}/', ' ', $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3])
);
}

if (preg_match('/^[\w-_\s]*[\w-_]+/', $identifier)) {
return $this->startQuote . $identifier . $this->endQuote;
}

return $identifier;
}

}
Expand Up @@ -537,4 +537,74 @@ public function testQuote() {
$this->assertEquals($expected, $result);
}

/**
* Tests identifier quoting
*
* @return void
*/
public function testQuoteIdentifier() {
$driver = $this->connection->driver();
$driver->startQuote = '"';
$driver->endQuote = '"';

$result = $this->connection->quoteIdentifier('name');
$expected = '"name"';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Model.*');
$expected = '"Model".*';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('MTD()');
$expected = 'MTD()';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('(sm)');
$expected = '(sm)';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('name AS x');
$expected = '"name" AS "x"';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Model.name AS x');
$expected = '"Model"."name" AS "x"';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Function(Something.foo)');
$expected = 'Function("Something"."foo")';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Function(SubFunction(Something.foo))');
$expected = 'Function(SubFunction("Something"."foo"))';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Function(Something.foo) AS x');
$expected = 'Function("Something"."foo") AS "x"';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('name-with-minus');
$expected = '"name-with-minus"';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('my-name');
$expected = '"my-name"';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Foo-Model.*');
$expected = '"Foo-Model".*';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Team.P%');
$expected = '"Team"."P%"';
$this->assertEquals($expected, $result);

$result = $this->connection->quoteIdentifier('Team.G/G');
$expected = '"Team"."G/G"';

$result = $this->connection->quoteIdentifier('Model.name as y');
$expected = '"Model"."name" AS "y"';
$this->assertEquals($expected, $result);
}

}

0 comments on commit acd33cb

Please sign in to comment.