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

709 lines
22 KiB
PHP

<?php
/**
* Turba directory driver implementation for an IMSP server.
*
* Copyright 2010-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (ASL). If you did
* did not receive this file, see http://www.horde.org/licenses/apache.
*
* @author Michael Rubinsky <mrubinsk@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Turba
*/
class Turba_Driver_Imsp extends Turba_Driver
{
/**
* Horde_Imsp object
*
* @var Horde_Imsp
*/
protected $_imsp;
/**
* The name of the addressbook.
*
* @var string
*/
protected $_bookName = '';
/**
* Holds if we are authenticated.
*
* @var boolean
*/
protected $_authenticated = '';
/**
* Holds name of the field indicating an IMSP group.
*
* @var string
*/
protected $_groupField = '';
/**
* Holds value that $_groupField will have if entry is an IMSP group.
*
* @var string
*/
protected $_groupValue = '';
/**
* Used to set if the current search is for contacts only.
*
* @var boolean
*/
protected $_noGroups = '';
/**
* Driver capabilities.
*
* @var array
*/
protected $_capabilities = array(
'delete_all' => true,
'delete_addressbook' => true
);
/**
* Constructs a new Turba imsp driver object.
*
* @param array $params Hash containing additional configuration
* parameters.
*/
public function __construct($name = '', $params)
{
parent::__construct($name, $params);
$this->params = $params;
$this->_groupField = $params['group_id_field'];
$this->_groupValue = $params['group_id_value'];
$this->_myRights = $params['my_rights'];
$this->_perms = $this->_aclToHordePerms($params['my_rights']);
$this->_bookName = $this->getContactOwner();
try {
$this->_imsp = $GLOBALS['injector']
->getInstance('Horde_Core_Factory_Imsp')
->create('Book', $this->params);
} catch (Horde_Exception $e) {
$this->_authenticated = false;
throw new Turba_Exception($e);
}
$this->_authenticated = true;
}
/**
* Returns all entries matching $critera.
*
* @param array $criteria Array containing the search criteria.
* @param array $fields List of fields to return.
*
* @return array Hash containing the search results.
*/
protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only)
{
$query = $results = array();
if (!$this->_authenticated) {
return $query;
}
/* Get the search criteria. */
if (count($criteria)) {
foreach ($criteria as $key => $vals) {
$names = (strval($key) == 'OR')
? $this->_doSearch($vals, 'OR')
: $this->_doSearch($vals, 'AND');
}
}
/* Now we have a list of names, get the rest. */
$result = $this->_read('name', $names, null, $fields);
if (is_array($result)) {
$results = $result;
}
Horde::log(sprintf('IMSP returned %s results', count($results)), 'DEBUG');
return $count_only ? count($results) : array_values($results);
}
/**
* Reads the given data from the address book and returns the results.
*
* @param string $key The primary key field to use (always 'name'
* for IMSP).
* @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.
*
* @return array Hash containing the search results.
* @throws Turba_Exception
*/
protected function _read($key, $ids, $owner, array $fields,
array $blobFields = array())
{
$results = array();
if (!$this->_authenticated) {
return $results;
}
$ids = array_values($ids);
$idCount = count($ids);
$IMSPGroups = $members = $tmembers = array();
for ($i = 0; $i < $idCount; ++$i) {
$result = array();
try {
$temp = isset($IMSPGroups[$ids[$i]])
? $IMSPGroups[$ids[$i]]
: $this->_imsp->getEntry($this->_bookName, $ids[$i]);
} catch (Horde_Imsp_Exception $e) {
continue;
}
$temp['fullname'] = $temp['name'];
$isIMSPGroup = false;
if (!isset($temp['__owner'])) {
$temp['__owner'] = $GLOBALS['registry']->getAuth();
}
if ((isset($temp[$this->_groupField])) &&
($temp[$this->_groupField] == $this->_groupValue)) {
if ($this->_noGroups) {
continue;
}
if (!isset($IMSPGroups[$ids[$i]])) {
$IMSPGroups[$ids[$i]] = $temp;
}
// move group ids to end of list
if ($idCount > count($IMSPGroups) &&
$idCount - count($IMSPGroups) > $i) {
$ids[] = $ids[$i];
unset($ids[$i]);
$ids = array_values($ids);
--$i;
continue;
}
$isIMSPGroup = true;
}
// Get the group members that might have been added from other
// IMSP applications, but only if we need more information than
// the group name
if ($isIMSPGroup &&
array_search('__members', $fields) !== false) {
if (isset($temp['email'])) {
$emailList = $this->_getGroupEmails($temp['email']);
$count = count($emailList);
for ($j = 0; $j < $count; ++$j) {
$needMember = true;
foreach ($results as $curResult) {
if (!empty($curResult['email']) &&
strtolower($emailList[$j]) == strtolower(trim($curResult['email']))) {
$members[] = $curResult['name'];
$needMember = false;
}
}
if ($needMember) {
$memberName = $this->_imsp->search
($this->_bookName,
array('email' => trim($emailList[$j])));
if (count($memberName)) {
$members[] = $memberName[0];
}
}
}
}
if (!empty($temp['__members'])) {
$tmembers = @unserialize($temp['__members']);
}
// TODO: Make sure that we are using the correct naming
// convention for members regardless of if we are using
// shares or not. This is needed to assure groups created
// while not using shares won't be lost when transitioning
// to shares and visa versa.
//$tmembers = $this->_checkMemberFormat($tmembers);
$temp['__members'] = serialize($this->_removeDuplicated(
array($members, $tmembers)));
$temp['__type'] = 'Group';
$temp['email'] = null;
$result = $temp;
} else {
// IMSP contact.
$count = count($fields);
for ($j = 0; $j < $count; ++$j) {
if (isset($temp[$fields[$j]])) {
$result[$fields[$j]] = $temp[$fields[$j]];
}
}
}
$results[] = $result;
}
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())
{
/* We need to map out Turba_Object_Groups back to IMSP groups before
* writing out to the server. We need to array_values() it in
* case an entry was deleted from the group. */
if ($attributes['__type'] == 'Group') {
/* We may have a newly created group. */
$attributes[$this->_groupField] = $this->_groupValue;
if (!isset($attributes['__members'])) {
$attributes['__members'] = '';
$attributes['email'] = ' ';
}
$temp = unserialize($attributes['__members']);
if (is_array($temp)) {
$members = array_values($temp);
} else {
$members = array();
}
// This searches the current IMSP address book to see if
// we have a match for this member before adding to email
// attribute since IMSP groups in other IMSP aware apps
// generally require an existing conact entry in the current
// address book for each group member (this is necessary for
// those sources that may be used both in AND out of Horde).
try {
$result = $this->_read('name', $members, null, array('email'));
$count = count($result);
for ($i = 0; $i < $count; ++$i) {
if (isset($result[$i]['email'])) {
$contact = sprintf("%s<%s>\n", $members[$i],
$result[$i]['email']);
$attributes['email'] .= $contact;
}
}
} catch (Turba_Exception $e) {}
}
unset($attributes['__type'], $attributes['fullname']);
if (!$this->params['contact_ownership']) {
unset($attributes['__owner']);
}
return $this->_imsp->addEntry($this->_bookName, $attributes);
}
/**
* TODO
*/
protected function _canAdd()
{
return true;
}
/**
* Deletes the specified object from the IMSP server.
*
* @throws Turba_Exception
*/
protected function _delete($object_key, $object_id)
{
try {
$this->_imsp->deleteEntry($this->_bookName, $object_id);
} catch (Horde_Imsp_Exception $e) {
throw new Turba_Exception($e);
}
}
/**
* Deletes the address book represented by this driver from the IMSP server.
*
* @throws Turba_Exception
*/
protected function _deleteAll()
{
try {
$this->_imsp->deleteAddressbook($this->_bookName);
} catch (Horde_Imsp_Exception $e) {
throw new Turba_Exception($e);
}
}
/**
* Saves the specified object to the IMSP server.
*
* @param Turba_Object $object The object to save/update.
*
* @return string The object id, possibly updated.
* @throws Turba_Exception
*/
protected function _save($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());
/* Check if the key changed, because IMSP will just write out
* a new entry without removing the previous one. */
if ($attributes['name'] != $this->_makeKey($attributes)) {
$this->_delete($object_key, $attributes['name']);
$attributes['name'] = $this->_makeKey($attributes);
$object_id = $attributes['name'];
}
$this->_add($attributes);
return $object_id;
}
/**
* 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($attributes)
{
return $attributes['fullname'];
}
/**
* Parses out $emailText into an array of pure email addresses
* suitable for searching the IMSP datastore with.
*
* @param string $emailText Single string containing email addressses.
*
* @return array Pure email address.
*/
protected function _getGroupEmails($emailText)
{
preg_match_all("(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})", $emailText, $matches);
return $matches[0];
}
/**
* Parses the search criteria, requests the individual searches from the
* server and performs any necessary ANDs / ORs on the results.
*
* @param array $criteria Array containing the search criteria.
* @param string $glue Type of search to perform (AND / OR).
*
* @return array Array containing contact names that match $criteria.
*/
protected function _doSearch($criteria, $glue)
{
$results = array();
foreach ($criteria as $vals) {
if (!empty($vals['OR'])) {
$results[] = $this->_doSearch($vals['OR'], 'OR');
} elseif (!empty($vals['AND'])) {
$results[] = $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'])) {
$results[] = $this->_sendSearch($vals);
} else {
foreach ($vals as $test) {
if (!empty($test['OR'])) {
$results[] = $this->_doSearch($test['OR'], 'OR');
} elseif (!empty($test['AND'])) {
$results[] = $this->_doSearch($test['AND'], 'AND');
} else {
$results[] = $this->_doSearch(array($test), $glue);
}
}
}
}
}
return ($glue == 'AND')
? $this->_getDuplicated($results)
: $this->_removeDuplicated($results);
}
/**
* Sends a search request to the server.
*
* @param array $criteria Array containing the search critera.
*
* @return array Array containing a list of names that match the search.
*/
function _sendSearch($criteria)
{
$names = '';
$imspSearch = array();
$searchkey = $criteria['field'];
$searchval = $criteria['test'];
$searchop = $criteria['op'];
$hasName = false;
$this->_noGroups = false;
$cache = $GLOBALS['injector']->getInstance('Horde_Cache');
$key = implode(".", array_merge($criteria, array($this->_bookName)));
/* Now make sure we aren't searching on a dynamically created
* field. */
switch ($searchkey) {
case 'fullname':
if (!$hasName) {
$searchkey = 'name';
$hasName = true;
} else {
$searchkey = '';
}
break;
case '__owner':
if (!$this->params['contact_ownership']) {
$searchkey = '';
$hasName = true;
}
break;
}
/* Are we searching for only Turba_Object_Groups or Turba_Objects?
* This is needed so the 'Show Lists' and 'Show Contacts'
* links work correctly in Turba. */
if ($searchkey == '__type') {
switch ($searchval) {
case 'Group':
$searchkey = $this->_groupField;
$searchval = $this->_groupValue;
break;
case 'Object':
if (!$hasName) {
$searchkey = 'name';
$searchval = '';
$hasName = true;
} else {
$searchkey = '';
}
$this->_noGroups = true;
break;
}
}
if (!$searchkey == '') {
// Check $searchval for content and for strict matches.
if (strlen($searchval) > 0) {
if ($searchop == 'LIKE') {
$searchval = '*' . $searchval . '*';
}
} else {
$searchval = '*';
}
$imspSearch[$searchkey] = $searchval;
}
if (!count($imspSearch)) {
$imspSearch['name'] = '*';
}
/* Finally get to the command. Check the cache first, since each
* 'Turba' search may consist of a number of identical IMSP
* searchaddress calls in order for the AND and OR parts to work
* correctly. 15 Second lifetime should be reasonable for this. This
* should reduce load on IMSP server somewhat.*/
$results = $cache->get($key, 15);
if ($results) {
$names = unserialize($results);
}
if (!$names) {
try {
$names = $this->_imsp->search($this->_bookName, $imspSearch);
$cache->set($key, serialize($names));
return $names;
} catch (Horde_Imsp_Exception $e) {
$GLOBALS['notification']->push($names, 'horde.error');
}
} else {
return $names;
}
}
/**
* Returns only those names that are duplicated in $names
*
* @param array $names A nested array of arrays containing names
*
* @return array Array containing the 'AND' of all arrays in $names
*/
protected function _getDuplicated($names)
{
$matched = $results = array();
/* If there is only 1 array, simply return it. */
if (count($names) < 2) {
return $names[0];
}
for ($i = 0; $i < count($names); ++$i) {
if (is_array($names[$i])) {
$results = array_merge($results, $names[$i]);
}
}
$search = array_count_values($results);
foreach ($search as $key => $value) {
if ($value > 1) {
$matched[] = $key;
}
}
return $matched;
}
/**
* Returns an array with all duplicate names removed.
*
* @param array $names Nested array of arrays containing names.
*
* @return array Array containg the 'OR' of all arrays in $names.
*/
protected function _removeDuplicated($names)
{
$unames = array();
for ($i = 0; $i < count($names); ++$i) {
if (is_array($names[$i])) {
$unames = array_merge($unames, $names[$i]);
}
}
return array_unique($unames);
}
/**
* Checks if the current user has the requested permission
* on this source.
*
* @param integer $perm The permission to check for.
*
* @return boolean true if user has permission, false otherwise.
*/
public function hasPermission($perm)
{
return $this->_perms & $perm;
}
/**
* Converts an acl string to a Horde Permissions bitmask.
*
* @param string $acl A standard, IMAP style acl string.
*
* @return integer Horde Permissions bitmask.
*/
protected function _aclToHordePerms($acl)
{
$hPerms = 0;
if (strpos($acl, 'w') !== false) {
$hPerms |= Horde_Perms::EDIT;
}
if (strpos($acl, 'r') !== false) {
$hPerms |= Horde_Perms::READ;
}
if (strpos($acl, 'd') !== false) {
$hPerms |= Horde_Perms::DELETE;
}
if (strpos($acl, 'l') !== false) {
$hPerms |= Horde_Perms::SHOW;
}
return $hPerms;
}
/**
* Creates a new Horde_Share and creates the address book
* on the IMSP server.
*
* @param array The params for the share.
*
* @return Horde_Share The share object.
* @throws Turba_Exception
*/
public function createShare($share_id, $params)
{
$params['params']['name'] = $this->params['username'];
if (!isset($params['default']) || $params['default'] !== true) {
$params['params']['name'] .= '.' . $params['name'];
}
$result = Turba::createShare($share_id, $params);
try {
Horde_Core_Imsp_Utils::createBook($GLOBALS['cfgSources']['imsp'], $params['params']['name']);
} catch (Horde_Imsp_Exception $e) {
throw new Turba_Exception($e);
}
return $result;
}
/**
* Helper function to count the occurances of the ':' * delimiter in group
* member entries.
*
* @param string $in The group member entry.
*
* @return integer The number of ':' in $in.
*/
protected function _countDelimiters($in)
{
$cnt = $pos = 0;
while (($pos = strpos($in, ':', $pos + 1)) !== false) {
++$cnt;
}
return $cnt;
}
/**
* Returns the owner for this contact. For an IMSP source, this should be
* the name of the address book.
*
* @return string TODO
*/
protected function _getContactOwner()
{
return $this->params['name'];
}
/**
* Check if the passed in share is the default share for this source.
*
* @see turba/lib/Turba_Driver#checkDefaultShare($share, $srcconfig)
*
* @return TODO
*/
public function checkDefaultShare($share, $srcConfig)
{
$params = @unserialize($share->get('params'));
if (!isset($params['default'])) {
$params['default'] = ($params['name'] == $srcConfig['params']['username']);
$share->set('params', serialize($params));
$share->save();
}
return $params['default'];
}
}