3
* Turba directory driver implementation for an IMSP server.
5
* Copyright 2010-2013 Horde LLC (http://www.horde.org/)
7
* See the enclosed file LICENSE for license information (ASL). If you did
8
* did not receive this file, see http://www.horde.org/licenses/apache.
10
* @author Michael Rubinsky <mrubinsk@horde.org>
12
* @license http://www.horde.org/licenses/apache ASL
15
class Turba_Driver_Imsp extends Turba_Driver
25
* The name of the addressbook.
29
protected $_bookName = '';
32
* Holds if we are authenticated.
36
protected $_authenticated = '';
39
* Holds name of the field indicating an IMSP group.
43
protected $_groupField = '';
46
* Holds value that $_groupField will have if entry is an IMSP group.
50
protected $_groupValue = '';
53
* Used to set if the current search is for contacts only.
57
protected $_noGroups = '';
60
* Driver capabilities.
64
protected $_capabilities = array(
66
'delete_addressbook' => true
70
* Constructs a new Turba imsp driver object.
72
* @param array $params Hash containing additional configuration
75
public function __construct($name = '', $params)
77
parent::__construct($name, $params);
79
$this->params = $params;
80
$this->_groupField = $params['group_id_field'];
81
$this->_groupValue = $params['group_id_value'];
82
$this->_myRights = $params['my_rights'];
83
$this->_perms = $this->_aclToHordePerms($params['my_rights']);
84
$this->_bookName = $this->getContactOwner();
87
$this->_imsp = $GLOBALS['injector']
88
->getInstance('Horde_Core_Factory_Imsp')
89
->create('Book', $this->params);
90
} catch (Horde_Exception $e) {
91
$this->_authenticated = false;
92
throw new Turba_Exception($e);
94
$this->_authenticated = true;
98
* Returns all entries matching $critera.
100
* @param array $criteria Array containing the search criteria.
101
* @param array $fields List of fields to return.
103
* @return array Hash containing the search results.
105
protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only)
107
$query = $results = array();
109
if (!$this->_authenticated) {
113
/* Get the search criteria. */
114
if (count($criteria)) {
115
foreach ($criteria as $key => $vals) {
116
$names = (strval($key) == 'OR')
117
? $this->_doSearch($vals, 'OR')
118
: $this->_doSearch($vals, 'AND');
122
/* Now we have a list of names, get the rest. */
123
$result = $this->_read('name', $names, null, $fields);
124
if (is_array($result)) {
128
Horde::logMessage(sprintf('IMSP returned %s results', count($results)), 'DEBUG');
130
return $count_only ? count($results) : array_values($results);
134
* Reads the given data from the address book and returns the results.
136
* @param string $key The primary key field to use (always 'name'
138
* @param mixed $ids The ids of the contacts to load.
139
* @param string $owner Only return contacts owned by this user.
140
* @param array $fields List of fields to return.
141
* @param array $blobFields Array of fields containing binary data.
143
* @return array Hash containing the search results.
144
* @throws Turba_Exception
146
protected function _read($key, $ids, $owner, array $fields,
147
array $blobFields = array())
151
if (!$this->_authenticated) {
155
$ids = array_values($ids);
156
$idCount = count($ids);
157
$IMSPGroups = $members = $tmembers = array();
159
for ($i = 0; $i < $idCount; ++$i) {
163
$temp = isset($IMSPGroups[$ids[$i]])
164
? $IMSPGroups[$ids[$i]]
165
: $this->_imsp->getEntry($this->_bookName, $ids[$i]);
166
} catch (Horde_Imsp_Exception $e) {
170
$temp['fullname'] = $temp['name'];
171
$isIMSPGroup = false;
172
if (!isset($temp['__owner'])) {
173
$temp['__owner'] = $GLOBALS['registry']->getAuth();
176
if ((isset($temp[$this->_groupField])) &&
177
($temp[$this->_groupField] == $this->_groupValue)) {
178
if ($this->_noGroups) {
181
if (!isset($IMSPGroups[$ids[$i]])) {
182
$IMSPGroups[$ids[$i]] = $temp;
184
// move group ids to end of list
185
if ($idCount > count($IMSPGroups) &&
186
$idCount - count($IMSPGroups) > $i) {
189
$ids = array_values($ids);
195
// Get the group members that might have been added from other
196
// IMSP applications, but only if we need more information than
199
array_search('__members', $fields) !== false) {
200
if (isset($temp['email'])) {
201
$emailList = $this->_getGroupEmails($temp['email']);
202
$count = count($emailList);
203
for ($j = 0; $j < $count; ++$j) {
205
foreach ($results as $curResult) {
206
if (!empty($curResult['email']) &&
207
strtolower($emailList[$j]) == strtolower(trim($curResult['email']))) {
208
$members[] = $curResult['name'];
213
$memberName = $this->_imsp->search
215
array('email' => trim($emailList[$j])));
217
if (count($memberName)) {
218
$members[] = $memberName[0];
223
if (!empty($temp['__members'])) {
224
$tmembers = @unserialize($temp['__members']);
227
// TODO: Make sure that we are using the correct naming
228
// convention for members regardless of if we are using
229
// shares or not. This is needed to assure groups created
230
// while not using shares won't be lost when transitioning
231
// to shares and visa versa.
232
//$tmembers = $this->_checkMemberFormat($tmembers);
234
$temp['__members'] = serialize($this->_removeDuplicated(
235
array($members, $tmembers)));
236
$temp['__type'] = 'Group';
237
$temp['email'] = null;
241
$count = count($fields);
242
for ($j = 0; $j < $count; ++$j) {
243
if (isset($temp[$fields[$j]])) {
244
$result[$fields[$j]] = $temp[$fields[$j]];
249
$results[] = $result;
256
* Adds the specified contact to the addressbook.
258
* @param array $attributes The attribute values of the contact.
259
* @param array $blob_fields TODO
261
* @throws Turba_Exception
263
protected function _add(array $attributes, array $blob_fields = array())
265
/* We need to map out Turba_Object_Groups back to IMSP groups before
266
* writing out to the server. We need to array_values() it in
267
* case an entry was deleted from the group. */
268
if ($attributes['__type'] == 'Group') {
269
/* We may have a newly created group. */
270
$attributes[$this->_groupField] = $this->_groupValue;
271
if (!isset($attributes['__members'])) {
272
$attributes['__members'] = '';
273
$attributes['email'] = ' ';
275
$temp = unserialize($attributes['__members']);
276
if (is_array($temp)) {
277
$members = array_values($temp);
282
// This searches the current IMSP address book to see if
283
// we have a match for this member before adding to email
284
// attribute since IMSP groups in other IMSP aware apps
285
// generally require an existing conact entry in the current
286
// address book for each group member (this is necessary for
287
// those sources that may be used both in AND out of Horde).
289
$result = $this->_read('name', $members, null, array('email'));
290
$count = count($result);
291
for ($i = 0; $i < $count; ++$i) {
292
if (isset($result[$i]['email'])) {
293
$contact = sprintf("%s<%s>\n", $members[$i],
294
$result[$i]['email']);
295
$attributes['email'] .= $contact;
298
} catch (Turba_Exception $e) {}
301
unset($attributes['__type'], $attributes['fullname']);
302
if (!$this->params['contact_ownership']) {
303
unset($attributes['__owner']);
306
return $this->_imsp->addEntry($this->_bookName, $attributes);
312
protected function _canAdd()
318
* Deletes the specified object from the IMSP server.
320
* @throws Turba_Exception
322
protected function _delete($object_key, $object_id)
325
$this->_imsp->deleteEntry($this->_bookName, $object_id);
326
} catch (Horde_Imsp_Exception $e) {
327
throw new Turba_Exception($e);
332
* Deletes the address book represented by this driver from the IMSP server.
334
* @throws Turba_Exception
336
protected function _deleteAll()
339
$this->_imsp->deleteAddressbook($this->_bookName);
340
} catch (Horde_Imsp_Exception $e) {
341
throw new Turba_Exception($e);
346
* Saves the specified object to the IMSP server.
348
* @param Turba_Object $object The object to save/update.
350
* @return string The object id, possibly updated.
351
* @throws Turba_Exception
353
protected function _save($object)
355
list($object_key, $object_id) = each($this->toDriverKeys(array('__key' => $object->getValue('__key'))));
356
$attributes = $this->toDriverKeys($object->getAttributes());
358
/* Check if the key changed, because IMSP will just write out
359
* a new entry without removing the previous one. */
360
if ($attributes['name'] != $this->_makeKey($attributes)) {
361
$this->_delete($object_key, $attributes['name']);
362
$attributes['name'] = $this->_makeKey($attributes);
363
$object_id = $attributes['name'];
366
$this->_add($attributes);
372
* Create an object key for a new object.
374
* @param array $attributes The attributes (in driver keys) of the
375
* object being added.
377
* @return string A unique ID for the new object.
379
protected function _makeKey($attributes)
381
return $attributes['fullname'];
385
* Parses out $emailText into an array of pure email addresses
386
* suitable for searching the IMSP datastore with.
388
* @param string $emailText Single string containing email addressses.
390
* @return array Pure email address.
392
protected function _getGroupEmails($emailText)
394
preg_match_all("(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})", $emailText, $matches);
399
* Parses the search criteria, requests the individual searches from the
400
* server and performs any necessary ANDs / ORs on the results.
402
* @param array $criteria Array containing the search criteria.
403
* @param string $glue Type of search to perform (AND / OR).
405
* @return array Array containing contact names that match $criteria.
407
protected function _doSearch($criteria, $glue)
410
foreach ($criteria as $vals) {
411
if (!empty($vals['OR'])) {
412
$results[] = $this->_doSearch($vals['OR'], 'OR');
413
} elseif (!empty($vals['AND'])) {
414
$results[] = $this->_doSearch($vals['AND'], 'AND');
416
/* If we are here, and we have a ['field'] then we
417
* must either do the 'AND' or the 'OR' search. */
418
if (isset($vals['field'])) {
419
$results[] = $this->_sendSearch($vals);
421
foreach ($vals as $test) {
422
if (!empty($test['OR'])) {
423
$results[] = $this->_doSearch($test['OR'], 'OR');
424
} elseif (!empty($test['AND'])) {
425
$results[] = $this->_doSearch($test['AND'], 'AND');
427
$results[] = $this->_doSearch(array($test), $glue);
434
return ($glue == 'AND')
435
? $this->_getDuplicated($results)
436
: $this->_removeDuplicated($results);
440
* Sends a search request to the server.
442
* @param array $criteria Array containing the search critera.
444
* @return array Array containing a list of names that match the search.
446
function _sendSearch($criteria)
449
$imspSearch = array();
450
$searchkey = $criteria['field'];
451
$searchval = $criteria['test'];
452
$searchop = $criteria['op'];
454
$this->_noGroups = false;
455
$cache = $GLOBALS['injector']->getInstance('Horde_Cache');
456
$key = implode(".", array_merge($criteria, array($this->_bookName)));
458
/* Now make sure we aren't searching on a dynamically created
460
switch ($searchkey) {
471
if (!$this->params['contact_ownership']) {
478
/* Are we searching for only Turba_Object_Groups or Turba_Objects?
479
* This is needed so the 'Show Lists' and 'Show Contacts'
480
* links work correctly in Turba. */
481
if ($searchkey == '__type') {
482
switch ($searchval) {
484
$searchkey = $this->_groupField;
485
$searchval = $this->_groupValue;
496
$this->_noGroups = true;
501
if (!$searchkey == '') {
502
// Check $searchval for content and for strict matches.
503
if (strlen($searchval) > 0) {
504
if ($searchop == 'LIKE') {
505
$searchval = '*' . $searchval . '*';
510
$imspSearch[$searchkey] = $searchval;
512
if (!count($imspSearch)) {
513
$imspSearch['name'] = '*';
516
/* Finally get to the command. Check the cache first, since each
517
* 'Turba' search may consist of a number of identical IMSP
518
* searchaddress calls in order for the AND and OR parts to work
519
* correctly. 15 Second lifetime should be reasonable for this. This
520
* should reduce load on IMSP server somewhat.*/
521
$results = $cache->get($key, 15);
524
$names = unserialize($results);
529
$names = $this->_imsp->search($this->_bookName, $imspSearch);
530
$cache->set($key, serialize($names));
532
} catch (Horde_Imsp_Exception $e) {
533
$GLOBALS['notification']->push($names, 'horde.error');
541
* Returns only those names that are duplicated in $names
543
* @param array $names A nested array of arrays containing names
545
* @return array Array containing the 'AND' of all arrays in $names
547
protected function _getDuplicated($names)
549
$matched = $results = array();
551
/* If there is only 1 array, simply return it. */
552
if (count($names) < 2) {
556
for ($i = 0; $i < count($names); ++$i) {
557
if (is_array($names[$i])) {
558
$results = array_merge($results, $names[$i]);
562
$search = array_count_values($results);
563
foreach ($search as $key => $value) {
573
* Returns an array with all duplicate names removed.
575
* @param array $names Nested array of arrays containing names.
577
* @return array Array containg the 'OR' of all arrays in $names.
579
protected function _removeDuplicated($names)
582
for ($i = 0; $i < count($names); ++$i) {
583
if (is_array($names[$i])) {
584
$unames = array_merge($unames, $names[$i]);
588
return array_unique($unames);
592
* Checks if the current user has the requested permission
595
* @param integer $perm The permission to check for.
597
* @return boolean true if user has permission, false otherwise.
599
public function hasPermission($perm)
601
return $this->_perms & $perm;
605
* Converts an acl string to a Horde Permissions bitmask.
607
* @param string $acl A standard, IMAP style acl string.
609
* @return integer Horde Permissions bitmask.
611
protected function _aclToHordePerms($acl)
615
if (strpos($acl, 'w') !== false) {
616
$hPerms |= Horde_Perms::EDIT;
618
if (strpos($acl, 'r') !== false) {
619
$hPerms |= Horde_Perms::READ;
621
if (strpos($acl, 'd') !== false) {
622
$hPerms |= Horde_Perms::DELETE;
624
if (strpos($acl, 'l') !== false) {
625
$hPerms |= Horde_Perms::SHOW;
632
* Creates a new Horde_Share and creates the address book
633
* on the IMSP server.
635
* @param array The params for the share.
637
* @return Horde_Share The share object.
638
* @throws Turba_Exception
640
public function createShare($share_id, $params)
642
$params['params']['name'] = $this->params['username'];
643
if (!isset($params['default']) || $params['default'] !== true) {
644
$params['params']['name'] .= '.' . $params['name'];
647
$result = Turba::createShare($share_id, $params);
649
Horde_Core_Imsp_Utils::createBook($GLOBALS['cfgSources']['imsp'], $params['params']['name']);
650
} catch (Horde_Imsp_Exception $e) {
651
throw new Turba_Exception($e);
658
* Helper function to count the occurances of the ':' * delimiter in group
661
* @param string $in The group member entry.
663
* @return integer The number of ':' in $in.
665
protected function _countDelimiters($in)
668
while (($pos = strpos($in, ':', $pos + 1)) !== false) {
676
* Returns the owner for this contact. For an IMSP source, this should be
677
* the name of the address book.
679
* @return string TODO
681
protected function _getContactOwner()
683
return $this->params['name'];
687
* Check if the passed in share is the default share for this source.
689
* @see turba/lib/Turba_Driver#checkDefaultShare($share, $srcconfig)
693
public function checkDefaultShare($share, $srcConfig)
695
$params = @unserialize($share->get('params'));
696
if (!isset($params['default'])) {
697
$params['default'] = ($params['name'] == $srcConfig['params']['username']);
698
$share->set('params', serialize($params));
702
return $params['default'];