* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Turba
*/
class Turba_Driver_Sql extends Turba_Driver
{
/**
* What can this backend do?
*
* @var array
*/
protected $_capabilities = array(
'delete_addressbook' => true,
'delete_all' => true
);
/**
* count() cache.
*
* @var array
*/
protected $_countCache = array();
/**
* Handle for the current database connection.
*
* @var Horde_Db_Adapter
*/
protected $_db;
/**
* Constructor.
*
* @param string $name The source name
* @param array $params Additional parameters needed:
*
* 'db' - (Horde_Db_Adapter) A DB Adapter object.
*
*/
public function __construct($name = '', array $params = array())
{
if (empty($params['db'])) {
throw new InvalidArgumentException('Missing required Horde_Db_Adapter object');
}
$this->_db = $params['db'];
unset($params['db']);
parent::__construct($name, $params);
}
/**
* Returns the number of contacts of the current user in this address book.
*
* @return integer The number of contacts that the user owns.
*/
public function count()
{
$test = $this->getContactOwner();
if (!isset($this->_countCache[$test])) {
/* Build up the full query. */
$query = 'SELECT COUNT(*) FROM ' . $this->_params['table'] .
' WHERE ' . $this->toDriver('__owner') . ' = ?';
$values = array($test);
/* Run query. */
try {
$this->_countCache[$test] = $this->_db->selectValue($query, $values);
} catch (Horde_Db_Exception $e) {
$this->_countCache[$test] = 0;
}
}
return $this->_countCache[$test];
}
/**
* Searches the SQL database with the given criteria and returns a
* filtered list of results. If the criteria parameter is an empty array,
* all records will be returned.
*
* @param array $criteria Array containing the search criteria.
* @param array $fields List of fields to return.
* @param array $blobFields TODO
* @param boolean $count_only Only return the count of matching entries,
* not the entries themselves.
*
* @return array Hash containing the search results.
* @throws Turba_Exception
*/
protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only = false)
{
return $this->_internalSearch($criteria, $fields, $blobFields, array(), $count_only);
}
/**
* Searches the SQL database with the given criteria and returns a
* filtered list of results. If the criteria parameter is an empty array,
* all records will be returned.
*
* @param array $criteria Array containing the search criteria.
* @param array $fields List of fields to return.
* @param array $blobFields TODO
* @param array $appendWhere An additional where clause to append.
* Array should contain 'sql' and 'params'
* params are used as bind parameters.
* @param boolean $count_only Only return the count of matching entries,
* not the entries themselves.
*
* @return mixed array|integer Hash containing the search results or the
* count of matching entries.
* @throws Turba_Exception
*/
protected function _internalSearch(array $criteria, array $fields, $blobFields = array(), $appendWhere = array(), $count_only = false)
{
/* Build the WHERE clause. */
$where = '';
$values = array();
if (count($criteria) || !empty($this->_params['filter'])) {
foreach ($criteria as $key => $vals) {
if ($key == 'OR' || $key == 'AND') {
if (!empty($where)) {
$where .= ' ' . $key . ' ';
}
$binds = $this->_buildSearchQuery($key, $vals);
$where .= '(' . $binds[0] . ')';
$values += $binds[1];
}
}
$where = ' WHERE ' . $where;
if (count($criteria) && !empty($this->_params['filter'])) {
$where .= ' AND ';
}
if (!empty($this->_params['filter'])) {
$where .= $this->_params['filter'];
}
if (count($appendWhere)) {
$where .= ' AND ' . $appendWhere['sql'];
$values = array_merge($values, $appendWhere['params']);
}
} elseif (count($appendWhere)) {
$where = ' WHERE ' . $appendWhere['sql'];
$values = array_merge($values, $appendWhere['params']);
}
/* Build up the full query. */
try {
if ($count_only) {
return $this->_db->selectValue(
'SELECT COUNT(*) FROM ' . $this->_params['table'] . $where,
$values
);
}
return $this->_parseRead(
$blobFields,
$this->_db->selectAll(
'SELECT ' . implode(', ', $fields) . ' FROM ' . $this->_params['table'] . $where,
$values
)
);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when performing search."));
}
}
protected function _parseRead($blobFields, $result, $dateFields = array())
{
$results = array();
foreach ($result as $row) {
$entry = array();
foreach ($row as $field => $val) {
if (isset($blobFields[$field])) {
if (!isset($columns)) {
$columns = $this->_db->columns($this->_params['table']);
}
$entry[$field] = $columns[$field]->binaryToString($val);
} elseif (isset($dateFields[$field]) && !empty($val)) {
$d = new Horde_Date($val);
$entry[$field] = $this->_convertFromDriver(
$d->strftime($GLOBALS['attributes'][array_search($field, $this->map)]['params']['format_in'])
);
} else {
$entry[$field] = $this->_convertFromDriver($val);
}
}
$results[] = $entry;
}
return $results;
}
/**
* Prepares field lists for searchDuplicates().
*
* @param array $array A list of field names.
*
* @return array A prepared list of field names.
*/
protected function _buildFields($array)
{
foreach ($array as &$entry) {
$entry = is_array($entry)
? implode(',', $this->_buildFields($entry))
: 'a1.' . $entry;
}
return $array;
}
/**
* Builds the WHERE conditions for searchDuplicates().
*
* @param array $array A list of field names.
*
* @return array A list of WHERE conditions.
*/
protected function _buildWhere($array)
{
foreach ($array as &$entry) {
if (is_array($entry)) {
$entry = reset($entry);
}
$entry = 'a1.' . $entry . ' IS NOT NULL AND a1.' . $entry . ' <> \'\'';
}
return $array;
}
/**
* Builds the JOIN conditions for searchDuplicates().
*
* @param array $array A list of field names.
*
* @return array A list of JOIN conditions.
*/
protected function _buildJoin($array)
{
foreach ($array as &$entry) {
$entry = is_array($entry)
? implode(' AND ', $this->_buildJoin($entry))
: 'a1.' . $entry . ' = a2.' . $entry;
}
return $array;
}
/**
* Searches the current address book for duplicate entries.
*
* Duplicates are determined by comparing email and name or last name and
* first name values.
*
* @return array A hash with the following format:
*
* array('name' => array('John Doe' => Turba_List, ...), ...)
*
* @throws Turba_Exception
*/
public function searchDuplicates()
{
$owner = $this->getContactOwner();
$fields = array();
if (is_array($this->map['name'])) {
if (in_array('lastname', $this->map['name']['fields']) &&
isset($this->map['lastname'])) {
$field = array($this->map['lastname']);
if (in_array('firstname', $this->map['name']['fields']) &&
isset($this->map['firstname'])) {
$field[] = $this->map['firstname'];
}
$fields[] = $field;
}
} else {
$fields[] = $this->map['name'];
}
if (isset($this->map['email'])) {
$fields[] = $this->map['email'];
}
$nameFormat = $GLOBALS['prefs']->getValue('name_format');;
if ($nameFormat != 'first_last' && $nameFormat != 'last_first') {
$nameFormat = 'first_last';
}
$order = $this->_buildFields($fields);
$joins = $this->_buildJoin($fields);
$where = $this->_buildWhere($fields);
$duplicates = array();
for ($i = 0; $i < count($joins); $i++) {
/* Build up the full query. */
$values = array();
$query = sprintf('SELECT DISTINCT a1.%s, %s FROM %s a1 JOIN %s a2 ON %s AND a1.%s <> a2.%s WHERE',
$this->map['__key'],
$order[$i],
$this->_params['table'],
$this->_params['table'],
$joins[$i],
$this->map['__key'],
$this->map['__key']);
if (isset($this->map['__owner'])) {
$query .= sprintf(' a1.%s = ? AND a2.%s = ? AND',
$this->map['__owner'],
$this->map['__owner']);
$values = array($owner, $owner);
}
$query .= sprintf(' %s ORDER BY %s',
$where[$i],
$order[$i]);
/* Run query. */
try {
$ids = $this->_db->selectValues($query, $values);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when performing search."));
}
$field = ($i == 0)
? 'name'
: array_search($fields[$i], $this->map);
$contacts = array();
foreach ($ids as $id) {
$contact = $this->getObject($id);
$value = $contact->getValue($field);
if ($field == 'name') {
$value = Turba::formatName($contact, $nameFormat);
}
/* HACK! */
if ($field == 'email') {
$value = Horde_String::lower($value);
}
if (!isset($contacts[$value])) {
$contacts[$value] = new Turba_List();
}
$contacts[$value]->insert($contact);
}
if ($contacts) {
$duplicates[$field] = $contacts;
}
}
return $duplicates;
}
/**
* Reads the given data from the SQL database and returns the results.
*
* @param string $key The primary key field to use.
* @param mixed $ids The ids of the contacts to load.
* @param string $owner Only return contacts owned by this user.
* @param array $fields List of fields to return.
* @param array $blobFields Array of fields containing binary data.
* @param array $dateFields Array of fields containing date data.
* @since 4.2.0
*
* @return array Hash containing the search results.
* @throws Turba_Exception
*/
protected function _read($key, $ids, $owner, array $fields,
array $blobFields = array(), array $dateFields = array())
{
$values = array();
$in = '';
if (is_array($ids)) {
if (!count($ids)) {
return array();
}
foreach ($ids as $id) {
$in .= empty($in) ? '?' : ', ?';
$values[] = $this->_convertToDriver($id);
}
$where = $key . ' IN (' . $in . ')';
} else {
$where = $key . ' = ?';
$values[] = $this->_convertToDriver($ids);
}
if (isset($this->map['__owner'])) {
$where .= ' AND ' . $this->map['__owner'] . ' = ?';
$values[] = $this->_convertToDriver($owner);
}
if (!empty($this->_params['filter'])) {
$where .= ' AND ' . $this->_params['filter'];
}
$query = 'SELECT ' . implode(', ', $fields) . ' FROM '
. $this->_params['table'] . ' WHERE ' . $where;
try {
return $this->_parseRead($blobFields, $this->_db->selectAll($query, $values), $dateFields);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when performing search."));
}
}
/**
* Adds the specified object to the SQL database.
*
* @param array $attributes The attribute values of the contact.
* @param array $blob_fields Fields that represent binary data.
* @param array $date_fields Fields that represent dates. @since 4.2.0
*
* @throws Turba_Exception
*/
protected function _add(array $attributes, array $blob_fields = array(),
array $date_fields = array())
{
list($fields, $values) = $this->_prepareWrite(
$attributes,
$blob_fields,
$date_fields
);
try {
$this->_db->insertBlob(
$this->_params['table'],
array_combine($fields, $values)
);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when adding data."));
}
}
protected function _prepareWrite($attributes, $blob_fields, $date_fields)
{
$fields = $values = array();
foreach ($attributes as $field => $value) {
$fields[] = $field;
if (!empty($value) && isset($blob_fields[$field])) {
$values[] = new Horde_Db_Value_Binary($value);
} elseif (!empty($value) && isset($date_fields[$field])) {
$d = new Horde_Date($value);
$values[] = $d->strftime('%Y-%m-%d');
} else {
$values[] = $this->_convertToDriver($value);
}
}
return array($fields, $values);
}
/**
* TODO
*/
protected function _canAdd()
{
return true;
}
/**
* Deletes the specified object from the SQL database.
*
* @throws Turba_Exception
*/
protected function _delete($object_key, $object_id)
{
$query = 'DELETE FROM ' . $this->_params['table'] .
' WHERE ' . $object_key . ' = ?';
$values = array($object_id);
try {
$this->_db->delete($query, $values);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when deleting data."));
}
}
/**
* Deletes all contacts from a specific address book.
*
* @param string $sourceName The source to remove all contacts from.
*
* @return array An array of UIDs
* @throws Turba_Exception
*/
protected function _deleteAll($sourceName = null)
{
if (!$GLOBALS['registry']->getAuth()) {
throw new Turba_Exception('Permission denied');
}
/* Get owner id */
$values = empty($sourceName)
? array($GLOBALS['registry']->getAuth())
: array($sourceName);
if (empty($this->map['__owner'])) {
throw new Turba_Exception_NotSupported('Unable to find __owner field. Cannot delete.');
}
$owner_field = $this->map['__owner'];
/* Need a list of UIDs so we can notify History */
$query = sprintf('SELECT %s FROM %s WHERE %s = ?',
$this->map['__uid'], $this->_params['table'], $owner_field);
try {
$ids = $this->_db->selectValues($query, $values);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when deleting data."));
}
/* Do the deletion */
$query = sprintf('DELETE FROM %s WHERE %s = ?', $this->_params['table'], $owner_field);
try {
$this->_db->delete($query, $values);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when deleting data."));
}
return $ids;
}
/**
* Saves the specified object in the SQL database.
*
* @param Turba_Object $object The object to save.
*
* @return string The object id, possibly updated.
* @throws Turba_Exception
*/
protected function _save(Turba_Object $object)
{
$object_keys = $this->toDriverKeys(array('__key' => $object->getValue('__key')));
$object_id = reset($object_keys);
$object_key = key($object_keys);
$attributes = $this->toDriverKeys($object->getAttributes());
$blob_fields = $this->toDriverKeys($this->getBlobs());
$date_fields = $this->toDriverKeys($this->getDateFields());
unset($attributes[$object_key]);
list($fields, $values) = $this->_prepareWrite(
$attributes,
$blob_fields,
$date_fields
);
try {
$this->_db->updateBlob(
$this->_params['table'],
array_combine($fields, $values),
array($object_key . ' = ?', array($object_id))
);
} catch (Horde_Db_Exception $e) {
throw new Turba_Exception(_("Server error when saving data."));
}
return $object_id;
}
/**
* Creates an object key for a new object.
*
* @param array $attributes The attributes (in driver keys) of the
* object being added.
*
* @return string A unique ID for the new object.
*/
protected function _makeKey(array $attributes)
{
return strval(new Horde_Support_Randomid());
}
/**
* Builds a piece of a search query.
*
* @param string $glue The glue to join the criteria (OR/AND).
* @param array $criteria The array of criteria.
*
* @return array An SQL fragment and a list of values suitable for binding
* as an array.
*/
protected function _buildSearchQuery($glue, array $criteria)
{
$clause = '';
$values = array();
foreach ($criteria as $key => $vals) {
if (!empty($vals['OR']) || !empty($vals['AND'])) {
if (!empty($clause)) {
$clause .= ' ' . $glue . ' ';
}
$binds = $this->_buildSearchQuery(!empty($vals['OR']) ? 'OR' : 'AND', $vals);
$clause .= '(' . $binds[0] . ')';
$values = array_merge($values, $binds[1]);
} else {
if (isset($vals['field'])) {
if (!empty($clause)) {
$clause .= ' ' . $glue . ' ';
}
$rhs = $this->_convertToDriver($vals['test']);
$binds = $this->_db->buildClause($vals['field'], $vals['op'], $rhs, true, array('begin' => !empty($vals['begin'])));
if (is_array($binds)) {
$clause .= $binds[0];
$values = array_merge($values, $binds[1]);
} else {
$clause .= $binds;
}
} else {
foreach ($vals as $test) {
if (!empty($test['OR']) || !empty($test['AND'])) {
if (!empty($clause)) {
$clause .= ' ' . $glue . ' ';
}
$binds = $this->_buildSearchQuery(!empty($vals['OR']) ? 'OR' : 'AND', $test);
$clause .= '(' . $binds[0] . ')';
$values = array_merge($values, $binds[1]);
} else {
if (!empty($clause)) {
$clause .= ' ' . $key . ' ';
}
$rhs = $this->_convertToDriver($test['test']);
if ($rhs == '' && $test['op'] == '=') {
$clause .= '(' . $this->_db->buildClause($test['field'], '=', $rhs) . ' OR ' . $test['field'] . ' IS NULL)';
} else {
$binds = $this->_db->buildClause($test['field'], $test['op'], $rhs, true, array('begin' => !empty($test['begin'])));
if (is_array($binds)) {
$clause .= $binds[0];
$values = array_merge($values, $binds[1]);
} else {
$clause .= $binds;
}
}
}
}
}
}
}
return array($clause, $values);
}
/**
* Translates a hash from being keyed on driver-specific fields to being
* keyed on the generalized Turba attributes. The translation is based on
* the contents of $this->map.
*
* @param array $entry A hash using driver-specific keys.
*
* @return array Translated version of $entry.
*/
public function toTurbaKeys(array $entry)
{
$new_entry = array();
foreach ($this->map as $key => $val) {
$key = preg_replace('/.*\.(.*)/', '$1', $key);
if (!is_array($val)) {
$new_entry[$key] = (isset($entry[$val]) && (!empty($entry[$val]) || (is_string($entry[$val]) && strlen($entry[$val]))))
? trim($entry[$val])
: null;
}
}
return $new_entry;
}
/**
* Converts a value from the driver's charset to the default charset.
*
* @param mixed $value A value to convert.
*
* @return mixed The converted value.
*/
protected function _convertFromDriver($value)
{
return Horde_String::convertCharset($value, $this->_db->getOption('charset'), 'UTF-8');
}
/**
* Converts a value from the default charset to the driver's charset.
*
* @param mixed $value A value to convert.
*
* @return mixed The converted value.
*/
protected function _convertToDriver($value)
{
return Horde_String::convertCharset($value, 'UTF-8', $this->_db->getOption('charset'));
}
/**
* Remove all entries owned by the specified user.
*
* @param string $user The user's data to remove.
*
* @throws Horde_Exception_PermissionDenied
*/
public function removeUserData($user)
{
// Make sure we are being called by an admin.
if (!$GLOBALS['registry']->isAdmin()) {
throw new Horde_Exception_PermissionDenied(_("Permission denied"));
}
$this->_deleteAll($user);
}
/**
* Obtain Turba_List of items to get TimeObjects out of.
*
* @param Horde_Date $start The starting date.
* @param Horde_Date $end The ending date.
* @param string $field The address book field containing the
* timeObject information (birthday, anniversary)
*
* @return Turba_List Object list.
* @throws Turba_Exception
*/
public function getTimeObjectTurbaList(Horde_Date $start, Horde_Date $end, $field)
{
$t_object = $this->toDriver($field);
$criteria = $this->makesearch(
array('__owner' => $this->getContactOwner()),
'AND',
array($this->toDriver('__owner') => true),
false);
// Limit to entries that actually contain a birthday and that are in the
// date range we are looking for.
$criteria['AND'][] = array(
'field' => $t_object,
'op' => '<>',
'test' => ''
);
$criteria['AND'][] = array(
'field' => $t_object,
'op' => '<>',
'test' => '0000-00-00'
);
if ($start->year == $end->year) {
$start = sprintf('%02d-%02d', $start->month, $start->mday);
$end = sprintf('%02d-%02d', $end->month, $end->mday);
$where = array('sql' => $t_object . ' IS NOT NULL AND SUBSTR('
. $t_object . ', 6, 5) BETWEEN ? AND ?',
'params' => array($start, $end));
} else {
$months = array();
$diff = ($end->month + 12) - $start->month;
$newDate = new Horde_Date(array(
'month' => $start->month,
'mday' => $start->mday,
'year' => $start->year
));
for ($i = 0; $i <= $diff; ++$i) {
$months[] = sprintf('%02d', $newDate->month++);
}
$where = array('sql' => $t_object . ' IS NOT NULL AND SUBSTR('
. $t_object . ', 6, 2) IN ('
. str_repeat('?,', count($months) - 1) . '?)',
'params' => $months);
}
$fields_pre = array(
'__key', '__type', '__owner', 'name', 'birthday', 'anniversary'
);
$fields = array();
foreach ($fields_pre as $field) {
$result = $this->toDriver($field);
if (is_array($result)) {
foreach ($result as $composite_field) {
$composite_result = $this->toDriver($composite_field);
if ($composite_result) {
$fields[] = $composite_result;
}
}
} elseif ($result) {
$fields[] = $result;
}
}
return $this->_toTurbaObjects($this->_internalSearch($criteria, $fields, array(), $where));
}
}