599 lines
19 KiB
PHP
599 lines
19 KiB
PHP
<?php
|
|
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
|
|
|
|
namespace Icinga\Data\Db;
|
|
|
|
use Exception;
|
|
use Icinga\Data\Inspectable;
|
|
use Icinga\Data\Inspection;
|
|
use PDO;
|
|
use Iterator;
|
|
use Zend_Db;
|
|
use Zend_Db_Expr;
|
|
use Icinga\Data\ConfigObject;
|
|
use Icinga\Data\Extensible;
|
|
use Icinga\Data\Filter\Filter;
|
|
use Icinga\Data\Filter\FilterAnd;
|
|
use Icinga\Data\Filter\FilterNot;
|
|
use Icinga\Data\Filter\FilterOr;
|
|
use Icinga\Data\Reducible;
|
|
use Icinga\Data\ResourceFactory;
|
|
use Icinga\Data\Selectable;
|
|
use Icinga\Data\Updatable;
|
|
use Icinga\Exception\ConfigurationError;
|
|
use Icinga\Exception\ProgrammingError;
|
|
|
|
/**
|
|
* Encapsulate database connections and query creation
|
|
*/
|
|
class DbConnection implements Selectable, Extensible, Updatable, Reducible, Inspectable
|
|
{
|
|
/**
|
|
* Connection config
|
|
*
|
|
* @var ConfigObject
|
|
*/
|
|
private $config;
|
|
|
|
/**
|
|
* Database type
|
|
*
|
|
* @var string
|
|
*/
|
|
private $dbType;
|
|
|
|
/**
|
|
* @var \Zend_Db_Adapter_Abstract
|
|
*/
|
|
private $dbAdapter;
|
|
|
|
/**
|
|
* Table prefix
|
|
*
|
|
* @var string
|
|
*/
|
|
private $tablePrefix = '';
|
|
|
|
private static $genericAdapterOptions = array(
|
|
Zend_Db::AUTO_QUOTE_IDENTIFIERS => false,
|
|
Zend_Db::CASE_FOLDING => Zend_Db::CASE_LOWER
|
|
);
|
|
|
|
private static $driverOptions = array(
|
|
PDO::ATTR_TIMEOUT => 10,
|
|
PDO::ATTR_CASE => PDO::CASE_LOWER,
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
);
|
|
|
|
/**
|
|
* Create a new connection object
|
|
*
|
|
* @param ConfigObject $config
|
|
*/
|
|
public function __construct(ConfigObject $config = null)
|
|
{
|
|
$this->config = $config;
|
|
$this->connect();
|
|
}
|
|
|
|
/**
|
|
* Provide a query on this connection
|
|
*
|
|
* @return DbQuery
|
|
*/
|
|
public function select()
|
|
{
|
|
return new DbQuery($this);
|
|
}
|
|
|
|
/**
|
|
* Fetch and return all rows of the given query's result set using an iterator
|
|
*
|
|
* @param DbQuery $query
|
|
*
|
|
* @return Iterator
|
|
*/
|
|
public function query(DbQuery $query)
|
|
{
|
|
return $query->getSelectQuery()->query();
|
|
}
|
|
|
|
/**
|
|
* Get the connection configuration
|
|
*
|
|
* @return ConfigObject
|
|
*/
|
|
public function getConfig()
|
|
{
|
|
return $this->config;
|
|
}
|
|
|
|
/**
|
|
* Getter for database type
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getDbType()
|
|
{
|
|
return $this->dbType;
|
|
}
|
|
|
|
/**
|
|
* Getter for the Zend_Db_Adapter
|
|
*
|
|
* @return \Zend_Db_Adapter_Abstract
|
|
*/
|
|
public function getDbAdapter()
|
|
{
|
|
return $this->dbAdapter;
|
|
}
|
|
|
|
/**
|
|
* Create a new connection
|
|
*/
|
|
private function connect()
|
|
{
|
|
$genericAdapterOptions = self::$genericAdapterOptions;
|
|
$driverOptions = self::$driverOptions;
|
|
$adapterParamaters = array(
|
|
'host' => $this->config->host,
|
|
'username' => $this->config->username,
|
|
'password' => $this->config->password,
|
|
'dbname' => $this->config->dbname,
|
|
'charset' => $this->config->charset ?: null,
|
|
'options' => & $genericAdapterOptions,
|
|
'driver_options' => & $driverOptions
|
|
);
|
|
$this->dbType = strtolower($this->config->get('db', 'mysql'));
|
|
switch ($this->dbType) {
|
|
case 'mssql':
|
|
$adapter = 'Pdo_Mssql';
|
|
$pdoType = $this->config->get('pdoType');
|
|
if (empty($pdoType)) {
|
|
if (extension_loaded('sqlsrv')) {
|
|
$adapter = 'Sqlsrv';
|
|
} else {
|
|
$pdoType = 'dblib';
|
|
}
|
|
}
|
|
if ($pdoType === 'dblib') {
|
|
// Driver does not support setting attributes
|
|
unset($adapterParamaters['options']);
|
|
unset($adapterParamaters['driver_options']);
|
|
}
|
|
if (! empty($pdoType)) {
|
|
$adapterParamaters['pdoType'] = $pdoType;
|
|
}
|
|
$defaultPort = 1433;
|
|
break;
|
|
case 'mysql':
|
|
$adapter = 'Pdo_Mysql';
|
|
if ($this->config->use_ssl) {
|
|
# The presence of these keys as empty strings or null cause non-ssl connections to fail
|
|
if ($this->config->ssl_key) {
|
|
$adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config->ssl_key;
|
|
}
|
|
if ($this->config->ssl_cert) {
|
|
$adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config->ssl_cert;
|
|
}
|
|
if ($this->config->ssl_ca) {
|
|
$adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config->ssl_ca;
|
|
}
|
|
if ($this->config->ssl_capath) {
|
|
$adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config->ssl_capath;
|
|
}
|
|
if ($this->config->ssl_cipher) {
|
|
$adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config->ssl_cipher;
|
|
}
|
|
}
|
|
/*
|
|
* Set MySQL server SQL modes to behave as closely as possible to Oracle and PostgreSQL. Note that the
|
|
* ONLY_FULL_GROUP_BY mode is left on purpose because MySQL requires you to specify all non-aggregate
|
|
* columns in the group by list even if the query is grouped by the master table's primary key which is
|
|
* valid ANSI SQL though. Further in that case the query plan would suffer if you add more columns to
|
|
* the group by list.
|
|
*/
|
|
$driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] =
|
|
'SET SESSION SQL_MODE=\'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,'
|
|
. 'NO_AUTO_CREATE_USER,ANSI_QUOTES,PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION\'';
|
|
if (isset($adapterParamaters['charset'])) {
|
|
$driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', NAMES ' . $adapterParamaters['charset'];
|
|
unset($adapterParamaters['charset']);
|
|
}
|
|
$driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .=';';
|
|
$defaultPort = 3306;
|
|
break;
|
|
case 'oci':
|
|
$adapter = 'Oracle';
|
|
unset($adapterParamaters['options']);
|
|
unset($adapterParamaters['driver_options']);
|
|
$adapterParamaters['driver_options'] = array(
|
|
'lob_as_string' => true
|
|
);
|
|
$defaultPort = 1521;
|
|
break;
|
|
case 'oracle':
|
|
$adapter = 'Pdo_Oci';
|
|
$defaultPort = 1521;
|
|
break;
|
|
case 'pgsql':
|
|
$adapter = 'Pdo_Pgsql';
|
|
$defaultPort = 5432;
|
|
break;
|
|
case 'ibm':
|
|
$adapter = 'Pdo_Ibm';
|
|
$defaultPort = 50000;
|
|
break;
|
|
case 'sqlite':
|
|
$adapter = 'Pdo_Sqlite';
|
|
$defaultPort = 0; // Dummy port because a value is required
|
|
break;
|
|
default:
|
|
throw new ConfigurationError(
|
|
'Backend "%s" is not supported',
|
|
$this->dbType
|
|
);
|
|
}
|
|
$adapterParamaters['port'] = $this->config->get('port', $defaultPort);
|
|
$this->dbAdapter = Zend_Db::factory($adapter, $adapterParamaters);
|
|
$this->dbAdapter->setFetchMode(Zend_Db::FETCH_OBJ);
|
|
// TODO(el/tg): The profiler is disabled per default, why do we disable the profiler explicitly?
|
|
$this->dbAdapter->getProfiler()->setEnabled(false);
|
|
}
|
|
|
|
public static function fromResourceName($name)
|
|
{
|
|
return new static(ResourceFactory::getResourceConfig($name));
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use Connection::getDbAdapter() instead
|
|
*/
|
|
public function getConnection()
|
|
{
|
|
return $this->dbAdapter;
|
|
}
|
|
|
|
/**
|
|
* Getter for the table prefix
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getTablePrefix()
|
|
{
|
|
return $this->tablePrefix;
|
|
}
|
|
|
|
/**
|
|
* Setter for the table prefix
|
|
*
|
|
* @param string $prefix
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setTablePrefix($prefix)
|
|
{
|
|
$this->tablePrefix = $prefix;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Count all rows of the result set
|
|
*
|
|
* @param DbQuery $query
|
|
*
|
|
* @return int
|
|
*/
|
|
public function count(DbQuery $query)
|
|
{
|
|
return (int) $this->dbAdapter->fetchOne($query->getCountQuery());
|
|
}
|
|
|
|
/**
|
|
* Retrieve an array containing all rows of the result set
|
|
*
|
|
* @param DbQuery $query
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchAll(DbQuery $query)
|
|
{
|
|
return $this->dbAdapter->fetchAll($query->getSelectQuery());
|
|
}
|
|
|
|
/**
|
|
* Fetch the first row of the result set
|
|
*
|
|
* @param DbQuery $query
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function fetchRow(DbQuery $query)
|
|
{
|
|
return $this->dbAdapter->fetchRow($query->getSelectQuery());
|
|
}
|
|
|
|
/**
|
|
* Fetch the first column of all rows of the result set as an array
|
|
*
|
|
* @param DbQuery $query
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchColumn(DbQuery $query)
|
|
{
|
|
return $this->dbAdapter->fetchCol($query->getSelectQuery());
|
|
}
|
|
|
|
/**
|
|
* Fetch the first column of the first row of the result set
|
|
*
|
|
* @param DbQuery $query
|
|
*
|
|
* @return string
|
|
*/
|
|
public function fetchOne(DbQuery $query)
|
|
{
|
|
return $this->dbAdapter->fetchOne($query->getSelectQuery());
|
|
}
|
|
|
|
/**
|
|
* Fetch all rows of the result set as an array of key-value pairs
|
|
*
|
|
* The first column is the key, the second column is the value.
|
|
*
|
|
* @param DbQuery $query
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchPairs(DbQuery $query)
|
|
{
|
|
return $this->dbAdapter->fetchPairs($query->getSelectQuery());
|
|
}
|
|
|
|
/**
|
|
* Insert a table row with the given data
|
|
*
|
|
* Note that the base implementation does not perform any quoting on the $table argument.
|
|
* Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
|
|
* as third parameter $types to define a different type than string for a particular column.
|
|
*
|
|
* @param string $table
|
|
* @param array $bind
|
|
* @param array $types
|
|
*
|
|
* @return int The number of affected rows
|
|
*/
|
|
public function insert($table, array $bind, array $types = array())
|
|
{
|
|
$columns = $values = array();
|
|
foreach ($bind as $column => $value) {
|
|
$columns[] = $column;
|
|
if ($value instanceof Zend_Db_Expr) {
|
|
$values[] = (string) $value;
|
|
unset($bind[$column]);
|
|
} else {
|
|
$values[] = ':' . $column;
|
|
}
|
|
}
|
|
|
|
$sql = 'INSERT INTO ' . $table
|
|
. ' (' . join(', ', $columns) . ') '
|
|
. 'VALUES (' . join(', ', $values) . ')';
|
|
$statement = $this->dbAdapter->prepare($sql);
|
|
|
|
foreach ($bind as $column => $value) {
|
|
$type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
|
|
$statement->bindValue(':' . $column, $value, $type);
|
|
}
|
|
|
|
$statement->execute();
|
|
return $statement->rowCount();
|
|
}
|
|
|
|
/**
|
|
* Update table rows with the given data, optionally limited by using a filter
|
|
*
|
|
* Note that the base implementation does not perform any quoting on the $table argument.
|
|
* Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
|
|
* as fourth parameter $types to define a different type than string for a particular column.
|
|
*
|
|
* @param string $table
|
|
* @param array $bind
|
|
* @param Filter $filter
|
|
* @param array $types
|
|
*
|
|
* @return int The number of affected rows
|
|
*/
|
|
public function update($table, array $bind, Filter $filter = null, array $types = array())
|
|
{
|
|
$set = array();
|
|
foreach ($bind as $column => $value) {
|
|
if ($value instanceof Zend_Db_Expr) {
|
|
$set[] = $column . ' = ' . $value;
|
|
unset($bind[$column]);
|
|
} else {
|
|
$set[] = $column . ' = :' . $column;
|
|
}
|
|
}
|
|
|
|
$sql = 'UPDATE ' . $table
|
|
. ' SET ' . join(', ', $set)
|
|
. ($filter ? ' WHERE ' . $this->renderFilter($filter) : '');
|
|
$statement = $this->dbAdapter->prepare($sql);
|
|
|
|
foreach ($bind as $column => $value) {
|
|
$type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
|
|
$statement->bindValue(':' . $column, $value, $type);
|
|
}
|
|
|
|
$statement->execute();
|
|
return $statement->rowCount();
|
|
}
|
|
|
|
/**
|
|
* Delete table rows, optionally limited by using a filter
|
|
*
|
|
* @param string $table
|
|
* @param Filter $filter
|
|
*
|
|
* @return int The number of affected rows
|
|
*/
|
|
public function delete($table, Filter $filter = null)
|
|
{
|
|
return $this->dbAdapter->delete($table, $filter ? $this->renderFilter($filter) : '');
|
|
}
|
|
|
|
/**
|
|
* Render and return the given filter as SQL-WHERE clause
|
|
*
|
|
* @param Filter $filter
|
|
*
|
|
* @return string
|
|
*/
|
|
public function renderFilter(Filter $filter, $level = 0)
|
|
{
|
|
// TODO: This is supposed to supersede DbQuery::renderFilter()
|
|
$where = '';
|
|
if ($filter->isChain()) {
|
|
if ($filter instanceof FilterAnd) {
|
|
$operator = ' AND ';
|
|
} elseif ($filter instanceof FilterOr) {
|
|
$operator = ' OR ';
|
|
} elseif ($filter instanceof FilterNot) {
|
|
$operator = ' AND ';
|
|
$where .= ' NOT ';
|
|
} else {
|
|
throw new ProgrammingError('Cannot render filter: %s', get_class($filter));
|
|
}
|
|
|
|
if (! $filter->isEmpty()) {
|
|
$parts = array();
|
|
foreach ($filter->filters() as $filterPart) {
|
|
$part = $this->renderFilter($filterPart, $level + 1);
|
|
if ($part) {
|
|
$parts[] = $part;
|
|
}
|
|
}
|
|
|
|
if (! empty($parts)) {
|
|
if ($level > 0) {
|
|
$where .= ' (' . implode($operator, $parts) . ') ';
|
|
} else {
|
|
$where .= implode($operator, $parts);
|
|
}
|
|
}
|
|
} else {
|
|
return ''; // Explicitly return the empty string due to the FilterNot case
|
|
}
|
|
} else {
|
|
$where .= $this->renderFilterExpression($filter);
|
|
}
|
|
|
|
return $where;
|
|
}
|
|
|
|
/**
|
|
* Render and return the given filter expression
|
|
*
|
|
* @param Filter $filter
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function renderFilterExpression(Filter $filter)
|
|
{
|
|
$column = $filter->getColumn();
|
|
$sign = $filter->getSign();
|
|
$value = $filter->getExpression();
|
|
|
|
if (is_array($value)) {
|
|
if ($sign === '=') {
|
|
return $column . ' IN (' . $this->dbAdapter->quote($value) . ')';
|
|
} elseif ($sign === '!=') {
|
|
return sprintf('(%1$s NOT IN (%2$s) OR %1$s IS NULL)', $column, $this->dbAdapter->quote($value));
|
|
}
|
|
|
|
throw new ProgrammingError(
|
|
'Unable to render array expressions with operators other than equal or not equal'
|
|
);
|
|
} elseif ($sign === '=' && strpos($value, '*') !== false) {
|
|
if ($value === '*') {
|
|
// We'll ignore such filters as it prevents index usage and because "*" means anything, so whether we're
|
|
// using a real column with a valid comparison here or just an expression which can only be evaluated to
|
|
// true makes no difference, except for performance reasons...
|
|
return new Zend_Db_Expr('TRUE');
|
|
}
|
|
|
|
return $column . ' LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value));
|
|
} elseif ($sign === '!=' && strpos($value, '*') !== false) {
|
|
if ($value === '*') {
|
|
// We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're
|
|
// using a real column with a valid comparison here or just an expression which cannot be evaluated to
|
|
// true makes no difference, except for performance reasons...
|
|
return new Zend_Db_Expr('FALSE');
|
|
}
|
|
|
|
return sprintf(
|
|
'(%1$s NOT LIKE %2$s OR %1$s IS NULL)',
|
|
$column,
|
|
$this->dbAdapter->quote(preg_replace('~\*~', '%', $value))
|
|
);
|
|
} elseif ($sign === '!=') {
|
|
return sprintf('(%1$s != %2$s OR %1$s IS NULL)', $column, $this->dbAdapter->quote($value));
|
|
} else {
|
|
return sprintf('%s %s %s', $column, $sign, $this->dbAdapter->quote($value));
|
|
}
|
|
}
|
|
|
|
public function inspect()
|
|
{
|
|
$insp = new Inspection('Db Connection');
|
|
try {
|
|
$this->getDbAdapter()->getConnection();
|
|
$config = $this->dbAdapter->getConfig();
|
|
$insp->write(sprintf(
|
|
'Connection to %s as %s on %s:%s successful',
|
|
$config['dbname'],
|
|
$config['username'],
|
|
$config['host'],
|
|
$config['port']
|
|
));
|
|
switch ($this->dbType) {
|
|
case 'mysql':
|
|
$rows = $this->dbAdapter->query(
|
|
'SHOW VARIABLES WHERE variable_name ' .
|
|
'IN (\'version\', \'protocol_version\', \'version_compile_os\', \'have_ssl\');'
|
|
)->fetchAll();
|
|
$sqlinsp = new Inspection('MySQL');
|
|
$hasSsl = false;
|
|
foreach ($rows as $row) {
|
|
$sqlinsp->write($row->variable_name . ': ' . $row->value);
|
|
if ($row->variable_name === 'have_ssl' && $row->value === 'YES') {
|
|
$hasSsl = true;
|
|
}
|
|
}
|
|
if ($hasSsl) {
|
|
$ssl_rows = $this->dbAdapter->query(
|
|
'SHOW STATUS WHERE variable_name ' .
|
|
'IN (\'Ssl_Cipher\');'
|
|
)->fetchAll();
|
|
foreach ($ssl_rows as $ssl_row) {
|
|
$sqlinsp->write($ssl_row->variable_name . ': ' . $ssl_row->value);
|
|
}
|
|
}
|
|
$insp->write($sqlinsp);
|
|
break;
|
|
case 'pgsql':
|
|
$row = $this->dbAdapter->query('SELECT version();')->fetchAll();
|
|
$sqlinsp = new Inspection('PostgreSQL');
|
|
$sqlinsp->write($row[0]->version);
|
|
$insp->write($sqlinsp);
|
|
break;
|
|
}
|
|
} catch (Exception $e) {
|
|
return $insp->error(sprintf('Connection failed %s', $e->getMessage()));
|
|
}
|
|
return $insp;
|
|
}
|
|
}
|