Files
server/usr/share/psa-horde/turba/lib/Driver/Kolab.php
2026-01-07 20:52:11 +01:00

1016 lines
33 KiB
PHP

<?php
/**
* Horde Turba driver for the Kolab IMAP Server.
*
* Copyright 2004-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (ASL). If you
* did not receive this file, see http://www.horde.org/licenses/apache.
*
* @author Thomas Jarosch <thomas.jarosch@intra2net.com>
* @author Gunnar Wrobel <wrobel@pardus.de>
* @author Stuart Binge <omicron@mighty.co.za>
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Turba
*/
class Turba_Driver_Kolab extends Turba_Driver
{
/**
* The Kolab_Storage backend.
*
* @var Horde_Kolab_Storage
*/
protected $_kolab;
/**
* Indicates if the driver has been connected to a specific addressbook or
* not.
*
* @var boolean
*/
protected $_connected = false;
/**
* The current addressbook.
*
* @var Horde_Kolab_Storage_Data
*/
protected $_data;
/**
* The current addressbook, serving groups.
*
* @var Horde_Kolab_Storage_Data
*/
protected $_listData;
/**
* The current addressbook represented as share.
*
* @var Horde_Share_Object
*/
protected $_share;
/**
* The cached contacts.
*
* @var array
*/
protected $_contacts_cache;
/**
* What can this backend do?
*
* @var array
*/
protected $_capabilities = array(
'delete_addressbook' => true,
'delete_all' => true,
);
/**
* Any additional options passed to Turba_Object constructors.
*
* @var array
*/
protected $_objectOptions = array('removeMissing' => true);
/**
* Attempts to open a Kolab Groupware folder.
*/
public function __construct($name = '', $params = array())
{
if (empty($params['storage'])) {
throw new InvalidArgumentException('Missing required storage handler.');
}
$this->_kolab = $params['storage'];
unset($params['storage']);
if (isset($params['share'])) {
$this->_share = $params['share'];
}
if (isset($params['name'])) {
$name = $params['name'];
}
parent::__construct($name, $params);
}
/**
* Translates the keys of the first hash from the generalized Turba
* attributes to the driver-specific fields. The translation is based on
* the contents of $this->map.
*
* @param array $hash Hash using Turba keys.
*
* @return array Translated version of $hash.
*/
public function toDriverKeys(array $hash)
{
if (isset($hash['__tags'])) {
if (!is_array($hash['__tags'])) {
$hash['__tags'] = $GLOBALS['injector']
->getInstance('Turba_Tagger')
->split($hash['__tags']);
}
usort($hash['__tags'], 'strcoll');
$hash['__internaltags'] = serialize($hash['__tags']);
}
$hash = parent::toDriverKeys($hash);
if (isset($hash['name'])) {
$hash['name'] = array('full-name' => $hash['name']);
}
/* TODO: use Horde_Kolab_Format_Xml_Type_Composite_* */
foreach (array('full-name',
'given-name',
'middle-names',
'last-name',
'initials',
'prefix',
'suffix') as $sub) {
if (isset($hash[$sub])) {
$hash['name'][$sub] = $hash[$sub];
unset($hash[$sub]);
}
}
if (isset($hash['__type']) && $hash['__type'] == 'Group' &&
isset($hash['name']['full-name'])) {
$hash['display-name'] = $hash['name']['full-name'];
}
if (isset($hash['emails'])) {
$list = new Horde_Mail_Rfc822_List($hash['emails']);
$hash['email'] = array();
foreach ($list as $address) {
$hash['email'][] = array('smtp-address' => $address->bare_address);
}
unset($hash['emails']);
}
foreach (array('phone-business1',
'phone-business2',
'phone-businessfax',
'phone-car',
'phone-company',
'phone-home1',
'phone-home2',
'phone-homefax',
'phone-mobile',
'phone-pager',
'phone-radio',
'phone-assistant') as $sub) {
if (isset($hash[$sub])) {
if (!isset($hash['phone'])) {
$hash['phone'] = array();
}
$hash['phone'][] = array('type' => substr($sub, 6),
'number' => $hash[$sub]);
unset($hash[$sub]);
}
}
$address = array();
foreach (array('addr-business-street',
'addr-business-locality',
'addr-business-region',
'addr-business-postal-code',
'addr-business-country') as $sub) {
if (isset($hash[$sub])) {
$address[substr($sub, 14)] = $hash[$sub];
unset($hash[$sub]);
}
}
if ($address) {
$hash['address'] = array();
$address['type'] = 'business';
$hash['address'][] = $address;
}
$address = array();
foreach (array('addr-home-street',
'addr-home-locality',
'addr-home-region',
'addr-home-postal-code',
'addr-home-country') as $sub) {
if (isset($hash[$sub])) {
$address[substr($sub, 10)] = $hash[$sub];
unset($hash[$sub]);
}
}
if ($address) {
if (!isset($hash['address'])) {
$hash['address'] = array();
}
$address['type'] = 'home';
$hash['address'][] = $address;
}
if (isset($hash['categories'])) {
$hash['categories'] = unserialize($hash['categories']);
}
if (!empty($hash['birthday'])) {
$hash['birthday'] = new DateTime($hash['birthday']);
}
if (!empty($hash['anniversary'])) {
$hash['anniversary'] = new DateTime($hash['anniversary']);
}
return $hash;
}
/**
* 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)
{
if (isset($entry['__type']) &&
$entry['__type'] == 'Group' &&
isset($entry['display-name'])) {
$entry['last-name'] = $entry['display-name'];
}
return parent::toTurbaKeys($entry);
}
/**
* Returns the Kolab data handler for contacts in the current address book.
*
* @return Horde_Kolab_Storage_Data The data handler.
*/
protected function _getData()
{
if ($this->_data === null) {
$this->_data = $this->_createData('contact');
}
return $this->_data;
}
/**
* Returns the Kolab data handler for distribution lists in the current
* address book.
*
* @return Horde_Kolab_Storage_Data The data handler.
*/
protected function _getListData()
{
if ($this->_listData === null) {
$this->_listData = $this->_createData('distribution-list');
}
return $this->_listData;
}
/**
* Returns a new Kolab data handler for the current address book.
*
* @param string $type An object type, either 'contact' or
* 'distribution-list'.
*
* @return Horde_Kolab_Storage_Data A data handler.
*/
protected function _createData($type)
{
if (!empty($this->_share)) {
$share = $this->_share;
} else {
if (empty($this->_name)) {
throw new Turba_Exception(
'The addressbook has been left undefined but is required!'
);
}
$share = $GLOBALS['injector']
->getInstance('Turba_Shares')
->getShare($this->_name);
}
$data = $this->_kolab->getData($share->get('folder'), $type);
$this->setContactOwner($share->get('owner'));
return $data;
}
/**
* Connect to the Kolab backend.
*
* @throws Turba_Exception
*/
public function connect()
{
if ($this->_connected) {
return;
}
/* Fetch the contacts first */
$raw_contacts = $this->_getData()->getObjects();
if (!$raw_contacts) {
$raw_contacts = array();
}
$contacts = array();
foreach ($raw_contacts as $id => $contact) {
if ($contact->getType() != 'contact') {
continue;
}
$backendId = $contact->getBackendId();
$contact = $contact->getData();
$contact['__type'] = 'Object';
$contact['__key'] = Horde_Url::uriB64Encode($id);
if (isset($contact['categories'])) {
$contact['categories'] = serialize($contact['categories']);
}
if (isset($contact['picture'])) {
$name = $contact['picture'];
$stream = $this->_getData()->getAttachment($backendId, $name);
if ($stream) {
$contact['photo'] = new Horde_Stream_Existing(array(
'stream' => $stream
));
foreach ($contact['_attachments']['type'] as $type => $list) {
if (array_search($name, $list) !== false) {
$contact['phototype'] = $type;
break;
}
}
}
}
if (isset($contact['name'])) {
foreach ($contact['name'] as $detail => $value) {
$contact[$detail] = $value;
}
unset($contact['name']);
}
if (isset($contact['phone'])) {
foreach ($contact['phone'] as $phone) {
$contact['phone-' . $phone['type']] = $phone['number'];
}
unset($contact['phone']);
}
if (isset($contact['email'])) {
$contact['emails'] = array();
foreach ($contact['email'] as $email) {
$contact['emails'][] = $email['smtp-address'];
}
if ($contact['emails']) {
$contact['email'] = $contact['emails'][0];
} else {
unset($contact['email']);
}
$contact['emails'] = implode(', ', $contact['emails']);
}
if (isset($contact['address'])) {
foreach ($contact['address'] as $address) {
foreach ($address as $detail => $value) {
if ($detail != 'type') {
$contact['addr-' . $address['type'] . '-' . $detail] = $value;
}
}
}
unset($contact['address']);
}
if (!empty($contact['birthday'])) {
$contact['birthday'] = $contact['birthday']->format('Y-m-d');
}
if (!empty($contact['anniversary'])) {
$contact['anniversary'] = $contact['anniversary']->format('Y-m-d');
}
$contacts[Horde_Url::uriB64Encode($id)] = $contact;
}
/* Now we retrieve distribution-lists */
$raw_groups = $this->_getListData()->getObjects();
$groups = array();
if ($raw_groups) {
foreach ($raw_groups as $id => $group) {
if ($group->getType() != 'distribution-list') {
continue;
}
$group = $group->getData();
$group['__type'] = 'Group';
$group['__key'] = Horde_Url::uriB64Encode($id);
if (isset($group['categories'])) {
$group['categories'] = serialize($group['categories']);
}
$groups[Horde_Url::uriB64Encode($id)] = $group;
}
}
/* Store the results in our cache */
$this->_contacts_cache = array_merge($contacts, $groups);
$this->_connected = true;
}
/**
* Searches the address book 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 Array of fields containing binary data.
*
* @return array Hash containing the search results.
* @throws Turba_Exception
*/
protected function _search(array $criteria, array $fields,
array $blobFields = array(), $count_only = false)
{
$this->connect();
if (!count($criteria)) {
return $count_only
? count($this->_contacts_cache)
: array_values($this->_contacts_cache);
}
// keep only entries matching criteria
$ids = array();
foreach ($criteria as $key => $criteria) {
$ids[] = $this->_doSearch($criteria, strval($key));
}
$ids = $this->_removeDuplicated($ids);
/* Now we have a list of names, get the rest. */
if ($ids) {
$result = $this->_read(
'uid',
array_map(array('Horde_Url', 'uriB64Encode'), $ids),
null,
$fields
);
} else {
$result = array();
}
Horde::log(sprintf('Kolab returned %s results', count($result)), 'DEBUG');
return $count_only ? count($result) : array_values($result);
}
/**
* Applies the filter criteria to a list of entries
*
* @param array $criteria Array containing the search criteria.
* @param array $fields List of fields to return.
*
* @return array Array containing the ids of the selected entries.
*/
protected function _doSearch($criteria, $glue)
{
$ids = array();
foreach ($criteria as $vals) {
if (!empty($vals['OR'])) {
$ids[] = $this->_doSearch($vals['OR'], 'OR');
} elseif (!empty($vals['AND'])) {
$ids[] = $this->_doSearch($vals['AND'], 'AND');
} else {
/* If we are here, and we have a ['field'] then we
* must either do the 'AND' or the 'OR' search. */
if (isset($vals['field'])) {
$ids[] = $this->_selectEntries($vals);
} else {
foreach ($vals as $test) {
if (!empty($test['OR'])) {
$ids[] = $this->_doSearch($test['OR'], 'OR');
} elseif (!empty($test['AND'])) {
$ids[] = $this->_doSearch($test['AND'], 'AND');
} else {
$ids[] = $this->_doSearch(array($test), $glue);
}
}
}
}
}
if ($glue == 'AND') {
$ids = $this->_getAND($ids);
} elseif ($glue == 'OR') {
$ids = $this->_removeDuplicated($ids);
}
return $ids;
}
/**
* Applies one filter criterium to a list of entries
*
* @param $test Test criterium
*
* @return array Array containing the ids of the selected entries
*/
protected function _selectEntries($test)
{
$ids = array();
if (!isset($test['field'])) {
Horde::log('Search field not set. Returning all entries.', 'DEBUG');
foreach ($this->_contacts_cache as $entry) {
$ids[] = $entry['uid'];
}
} else {
$field = $test['field'];
$value = isset($test['test'])
? $test['test']
: '';
// Special emails hack
if ($field == 'email') {
$field = 'emails';
$test['op'] = 'LIKE';
$test['begin'] = false;
}
if (!isset($test['op']) || $test['op'] == '=') {
foreach ($this->_contacts_cache as $entry) {
if (isset($entry[$field]) && $entry[$field] == $value) {
$ids[] = $entry['uid'];
}
}
} else {
// 'op' is LIKE
foreach ($this->_contacts_cache as $entry) {
if (empty($value) ||
(isset($entry[$field]) &&
((empty($test['begin']) &&
stripos($entry[$field], $value) !== false) ||
(!empty($test['begin']) &&
stripos($entry[$field], $value) === 0)))) {
$ids[] = $entry['uid'];
}
}
}
}
return $ids;
}
/**
* Returns only those names that are duplicated in $ids
*
* @param array $ids A nested array of arrays containing names
*
* @return array Array containing the 'AND' of all arrays in $ids
*/
protected function _getAND($ids)
{
$matched = $results = array();
/* If there is only 1 array, simply return it. */
if (count($ids) < 2) {
return $ids[0];
}
for ($i = 0; $i < count($ids); ++$i) {
if (is_array($ids[$i])) {
$results = array_merge($results, $ids[$i]);
}
}
$search = array_count_values($results);
foreach ($search as $key => $value) {
if ($value == count($ids)) {
$matched[] = $key;
}
}
return $matched;
}
/**
* Returns an array with all duplicate names removed.
*
* @param array $ids Nested array of arrays containing names.
*
* @return array Array containg the 'OR' of all arrays in $ids.
*/
protected function _removeDuplicated($ids)
{
$unames = array();
for ($i = 0; $i < count($ids); ++$i) {
if (is_array($ids[$i])) {
$unames = array_merge($unames, $ids[$i]);
}
}
return array_unique($unames);
}
/**
* Reads the given data from the address book 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())
{
$this->connect();
$results = array();
if (!is_array($ids)) {
$ids = array($ids);
}
$count = count($fields);
foreach ($ids as $id) {
if (!isset($this->_contacts_cache[$id])) {
continue;
}
$object = $this->_contacts_cache[$id];
if (!isset($object['__type']) || $object['__type'] == 'Object') {
if ($count) {
$result = array();
foreach ($fields as $field) {
if (isset($object[$field])) {
$result[$field] = $object[$field];
}
}
$results[] = $result;
} else {
$results[] = $object;
}
} else {
$member_ids = array();
if (isset($object['member'])) {
foreach ($object['member'] as $member) {
if (isset($member['uid'])) {
$id = Horde_Url::uriB64Encode($member['uid']);
$found = false;
try {
$this->getObject($id);
$found = true;
} catch (Horde_Exception_NotFound $e) {
foreach (Turba::listShares() as $share) {
$driver = $GLOBALS['injector']
->getInstance('Turba_Factory_Driver')
->create($share->getName());
try {
$driver->getObject($id);
$id = $share->getName() . ':' . $id;
$found = true;
break;
} catch (Horde_Exception_NotFound $e) {
continue;
}
}
}
if ($found) {
$member_ids[] = $id;
}
continue;
}
$display_name = $member['display-name'];
$smtp_address = $member['smtp-address'];
$criteria = array(
'AND' => array(
array(
'field' => 'full-name',
'op' => 'LIKE',
'test' => $display_name,
'begin' => false,
),
array(
'field' => 'emails',
'op' => 'LIKE',
'test' => $smtp_address,
'begin' => false,
),
),
);
$fields = array('uid');
// we expect only one result here!!!
$contacts = $this->_search($criteria, $fields);
// and drop everything else except the first search
// result
$member_ids[] = $contacts[0]['__key'];
}
$object['__members'] = serialize($member_ids);
unset($object['member']);
}
$results[] = $object;
}
}
if (!$results) {
throw new Horde_Exception_NotFound();
}
return $results;
}
/**
* Adds the specified contact to the addressbook.
*
* @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())
{
$this->connect();
$this->_store($attributes);
}
protected function _canAdd()
{
return true;
}
/**
* Removes the specified object from the Kolab message store.
*/
protected function _delete($object_key, $object_id)
{
$this->connect();
if ($object_key == 'uid') {
$object_id = Horde_Url::uriB64Encode($object_id);
} elseif ($object_key != '__key') {
throw new Turba_Exception(sprintf('Key for deleting must be a UID or ID not %s!', $object_key));
}
if (!isset($this->_contacts_cache[$object_id])) {
throw new Turba_Exception(sprintf(_("Object with UID %s does not exist!"), $object_id));
}
$data = isset($this->_contacts_cache[$object_id]['__type']) &&
$this->_contacts_cache[$object_id]['__type'] == 'Group'
? $this->_getListData()
: $this->_getData();
$result = $data->delete($this->_contacts_cache[$object_id]['uid']);
/* Invalidate cache. */
$this->_connected = false;
return $result;
}
/**
* 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)
{
$this->connect();
$uids = array_map(array('Horde_Url', 'uriB64Decode'), array_keys($this->_contacts_cache));
/* Delete contacts */
$this->_getData()->deleteAll();
$this->_getListData()->deleteAll();
return $uids;
}
/**
* 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)
{
$this->connect();
return $this->_store($this->toDriverKeys($object->getAttributes()),
$object->getValue('__uid'));
}
/**
* Stores an object in the Kolab message store.
*
* TODO
*
* @return string The object id, possibly updated.
* @throws Turba_Exception
*/
protected function _store($attributes, $object_id = null)
{
if (isset($attributes['__type']) && $attributes['__type'] == 'Group') {
$this->_convertMembers($attributes);
$data = $this->_getListData();
} else {
if (isset($attributes['photo']) && isset($attributes['phototype'])) {
$filename = 'photo.'
. Horde_Mime_Magic::mimeToExt($attributes['phototype']);
$attributes['_attachments'][$filename] = array(
'type' => $attributes['phototype'],
'content' => Horde_Stream_Wrapper_String::getStream($attributes['photo'])
);
$attributes['picture'] = $filename;
unset($attributes['photo'], $attributes['phototype']);
}
// EAS sets the date fields to '' instead of null -> fix it up
$fix_date_fields = array('birthday', 'anniversary');
foreach ($fix_date_fields as $fix_date) {
if (empty($attributes[$fix_date])) {
unset($attributes[$fix_date]);
}
}
$data = $this->_getData();
}
if ($object_id === null) {
$object_id = $data->create($attributes);
} else {
$data->modify($attributes);
}
/* Invalidate cache. */
$this->_connected = false;
return Horde_Url::uriB64Encode($object_id);
}
/**
* TODO
*/
protected function _convertMembers(&$attributes)
{
if (isset($attributes['__members'])) {
$member_ids = unserialize($attributes['__members']);
$attributes['member'] = array();
foreach ($member_ids as $member_id) {
$source_id = null;
if (strpos($member_id, ':')) {
list($source_id, $member_id) = explode(':', $member_id, 2);
}
$mail = array('uid' => Horde_Url::uriB64Decode($member_id));
$member = null;
if ($source_id) {
try {
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($source_id);
try {
$member = $driver->getObject($member_id);
$name = $member->getValue('name');
if (!empty($name)) {
$mail['display-name'] = $name;
}
$emails = $member->getValue('emails');
if ($emails) {
$emails = explode(',', $emails);
$mail['smtp-address'] = trim($emails[0]);
if (!isset($mail['display-name'])) {
$mail['display-name'] = $mail['smtp-address'];
}
}
} catch (Horde_Exception_NotFound $e) {
}
} catch (Turba_Exception $e) {
}
} elseif (isset($this->_contacts_cache[$member_id])) {
$member = $this->_contacts_cache[$member_id];
if (!empty($member['full-name'])) {
$mail['display-name'] = $member['full-name'];
}
if (!empty($member['emails'])) {
$emails = explode(',', $member['emails']);
$mail['smtp-address'] = trim($emails[0]);
if (!isset($mail['display-name'])) {
$mail['display-name'] = $mail['smtp-address'];
}
}
}
$attributes['member'][] = $mail;
}
unset($attributes['__members']);
}
}
/**
* Create 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 Horde_Url::uriB64Encode(
isset($attributes['uid'])
? $attributes['uid']
: $this->_generateUid());
}
/**
* Creates an object UID for a new object.
*
* @return string A unique ID for the new object.
*/
protected function _makeUid()
{
return $this->_generateUid();
}
/**
* Create an object key for a new object.
*
* @return string A unique ID for the new object.
*/
protected function _generateUid()
{
return $this->_getData()->generateUID();
}
/**
* Creates a new Horde_Share for this source type.
*
* @param string $share_name The share name
* @param array $params The params for the share.
*
* @return Horde_Share The share object.
*/
public function createShare($share_name, array $params)
{
if (!isset($params['name'])) {
$params['name'] = _('Contacts');
}
if (!empty($params['params']['default'])) {
$params['default'] = true;
unset($params['params']['default']);
}
return Turba::createShare($share_name, $params);
}
/**
* Check if the passed in share is the default share for this source.
*
* @param Horde_Share_Object $share The share object.
* @param array $srcconfig The cfgSource entry for the share.
*
* @return boolean TODO
*/
public function checkDefaultShare(Horde_Share_Object $share,
array $srcconfig)
{
$params = @unserialize($share->get('params'));
return isset($params['default'])
? $params['default']
: false;
}
/**
* Runs any actions after setting a new default notepad.
*
* @param string $share The default share ID.
*/
public function setDefaultShare($share)
{
$addressbooks = $GLOBALS['injector']->getInstance('Turba_Shares')
->listShares(
$GLOBALS['registry']->getAuth(),
array('perm' => Horde_Perms::SHOW,
'attributes' => $GLOBALS['registry']->getAuth()));
foreach ($addressbooks as $id => $addressbook) {
if ($id == $share) {
$addressbook->set('default', true);
$addressbook->save();
break;
}
}
}
}