709 lines
22 KiB
PHP
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'];
|
|
}
|
|
|
|
}
|