0011522: improve handling of group-lists
[tine20] / tine20 / Addressbook / Backend / Ldap.php
1 <?php
2
3 /**
4  * contacts ldap backend
5  * 
6  * @package     Addressbook
7  * @subpackage  Backend
8  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3 
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  * @copyright   Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
11  *
12  */
13
14 /**
15  * contacts ldap backend
16  * 
17  * NOTE: LDAP charset is always UTF-8 (RFC2253) so we don't have to cope with
18  *       charset conversions here ;-)
19  * 
20  * @package     Addressbook
21  * @subpackage  Backend
22  */
23 class Addressbook_Backend_Ldap implements Tinebase_Backend_Interface
24 {
25     /**
26      * backend type constant
27      */
28     const TYPE = 'Ldap';
29     
30     /**
31      * date representation used by ldap
32      */
33     const LDAPDATEFORMAT = 'YmdHis';
34     
35     /**
36      * ldap directory connection
37      *
38      * @var Tinebase_Ldap
39      */
40     protected $_ldap = NULL;
41     
42     /**
43      * base dn
44      *
45      * @var string
46      */
47     protected $_baseDn = NULL;
48     
49     /**
50      * options object 
51      * @see Zend_Ldap options + userDn + groupDn
52      *
53      * @var object
54      */
55     protected $_options = NULL;
56     
57     /**
58      * list of reqired schemas 
59      *
60      * @var array
61      */
62     protected $_requierdSchemas = array(
63         'posixaccount', 
64         'openldap', 
65         'inetorgperson'
66     );
67     
68     /**
69      * list of available schemas in ldap server
70      *
71      * @todo get this list from $this->_ldap
72      * @var array
73      */
74     protected $_availableSchemas = array(
75         'posixaccount', 
76         'openldap', 
77         'inetorgperson', 
78         'mozillaabpersonalpha', 
79         'evolutionperson'
80     );
81     
82     /**
83      * list of supportetd tine contact record fields
84      * 
85      * NOTE: this list depends on the supportet schemas by current ldap server
86      * @see $this->__attributesMaps
87      *
88      * @var array
89      */
90     protected $_supportedRecordFields = NULL;
91     
92     /**
93      * list of supported attributes by current ldap server
94      * 
95      * NOTE: dynamically determined by supported schemas
96      *
97      * @var array
98      */
99     protected $_supportedLdapAttributes = NULL;
100     
101     /**
102      * attributes mapping for supported schemas
103      * 
104      * NOTE: The mapping is _not_ one-to-one. one recordField could map to n ldapAttributes.
105      *       When reading a ldap entry we map the first non empty value.
106      *       When saving a ldap entry we write all n attributes with the same one record value //@todo to be discussed!
107      * 
108      * @var array schemaName => array (recordField => ldapAttribute)
109      */
110     protected  $_attributesMaps = array(
111         /**
112          * Abstraction of an account with POSIX attributes
113          * NOTE: For contacts we only use the reference to the account
114          */
115         'posixaccount' => array(
116             'account_id'    => 'uidnumber',
117         ),
118         
119         /**
120          * generic openldap attributes
121          */
122         'openldap' => array(
123             'id'                    => 'entryuuid',
124             'created_by'            => 'creatorsname',
125             'creation_time'         => 'createtimestamp',
126             'last_modified_by'      => 'modifiersname',
127             'last_modified_time'    => 'modifytimestamp',
128         ),
129         
130         /**
131          * RFC2798: Internet Organizational Person
132          */
133         'inetorgperson' => array(
134             'n_fn'                  => 'cn',
135             'n_given'               => 'givenname',
136             'n_family'              => 'sn',
137             'sound'                 => 'audio',
138             'note'                  => 'description',
139             'url'                   => 'labeleduri',
140             'org_name'              => 'o',
141             'org_unit'              => 'ou',
142             'title'                 => 'title',
143             'adr_one_street'        => 'street',
144             'adr_one_locality'      => 'l',
145             'adr_one_region'        => 'st',
146             'adr_one_postalcode'    => 'postalcode',
147             'tel_work'              => 'telephonenumber',
148             'tel_home'              => 'homephone',
149             'tel_fax'               => 'facsimiletelephonenumber',
150             'tel_cell'              => 'mobile',
151             'tel_pager'             => 'pager',
152             'email'                 => 'mail',
153             'room'                  => 'roomnumber',
154             'jpegphoto'             => 'jpegphoto',
155             'n_fileas'              => 'displayname',
156             'label'                 => 'postaladdress',
157             'pubkey'                => 'usersmimecertificate',
158         ),
159         
160         /**
161          * Mozilla LDAP Address Book Schema (alpha)
162          * 
163          * @link https://wiki.mozilla.org/MailNews:Mozilla_LDAP_Address_Book_Schema
164          * @link https://wiki.mozilla.org/MailNews:LDAP_Address_Books#LDAP_Address_Book_Schema
165          */
166         'mozillaabpersonalpha' => array(
167             'adr_one_street2'       => 'mozillaworkstreet2',
168             'adr_one_countryname'   => 'c', // 2 letter country code
169             'adr_two_street'        => 'mozillahomestreet',
170             'adr_two_street2'       => 'mozillahomestreet2',
171             'adr_two_locality'      => 'mozillahomelocalityname',
172             'adr_two_region'        => 'mozillahomestate',
173             'adr_two_postalcode'    => 'mozillahomepostalcode',
174             'adr_two_countryname'   => 'mozillahomecountryname',
175             'email_home'            => 'mozillasecondemail',
176             'url_home'              => 'mozillahomeurl',
177             //'' => 'displayName'
178             //'' => 'mozillaCustom1'
179             //'' => 'mozillaCustom2'
180             //'' => 'mozillaCustom3'
181             //'' => 'mozillaCustom4'
182             //'' => 'mozillaHomeUrl'
183             //'' => 'mozillaNickname'
184             //'' => 'mozillaUseHtmlMail'
185             //'' => 'nsAIMid'
186             //'' => 'postOfficeBox'
187         ),
188         
189         /**
190          * Mozilla LDAP Address Book Schema
191          * similar to the newer mozillaAbPerson, but uses mozillaPostalAddress2 instead of mozillaStreet2
192          * 
193          * @deprecated 
194          * @link https://bugzilla.mozilla.org/attachment.cgi?id=104858&action=view
195          */
196         'mozillaorgperson' => array(
197             'adr_one_street2'       => 'mozillapostaladdress2',
198             'adr_one_countryname'   => 'c',  // 2 letter country code
199             'adr_one_countryname'   => 'co', // human readable country name, must be after 'c' to take precedence on read!
200             'adr_two_street'        => 'mozillahomestreet',
201             'adr_two_street2'       => 'mozillahomepostaladdress2',
202             'adr_two_locality'      => 'mozillahomelocalityname',
203             'adr_two_region'        => 'mozillahomestate',
204             'adr_two_postalcode'    => 'mozillahomepostalcode',
205             'adr_two_countryname'   => 'mozillahomecountryname',
206             'email_home'            => 'mozillasecondemail',
207             'url_home'              => 'mozillahomeurl',
208         ),
209         
210         /**
211          * Objectclass geared to Evolution Usage
212          * 
213          * @link http://projects.gnome.org/evolution/index.shtml
214          */
215         'evolutionperson' => array(
216             'bday'          => 'birthdate',
217             'note'          => 'note',
218             'tel_car'       => 'carphone',
219             'tel_prefer'    => 'primaryphone',
220             'cat_id'        => 'category',  // special handling in _egw2evolutionperson method
221             'role'          => 'businessrole',
222             'tel_assistent' => 'assistantphone',
223             'assistent'     => 'assistantname',
224             'n_fileas'      => 'fileas',
225             'tel_fax_home'  => 'homefacsimiletelephonenumber',
226             'freebusy_uri'  => 'freeBusyuri',
227             'calendar_uri'  => 'calendaruri',
228             'tel_other'     => 'otherphone',
229             'tel_cell_private' => 'callbackphone',  // not the best choice, but better then nothing
230             //'' => 'managerName'
231             //'' => 'otherPostalAddress'
232             //'' => 'mailer'
233             //'' => 'anniversary'
234             //'' => 'spouseName'
235             //'' => 'companyPhone' 
236             //'' => 'otherFacsimileTelephoneNumber'
237             //'' => 'radio'
238             //'' => 'telex'
239             //'' => 'tty'
240             //'' => 'categories' //(deprecated)
241         ),
242         
243         /**
244          * unsupported tine fields:
245          */
246         //'is_deleted'            => '',
247         //'deleted_time'          => '',
248         //'deleted_by'            => '',
249         
250         //'container_id'          => '',
251         //'salutation'            => '',
252         
253         //'tz'                    => '',
254         //'geo'                   => '',
255         
256     );
257     
258     /**
259      * constructs a contacts ldap backend
260      *
261      * @param  array $options Options used in connecting, binding, etc.
262      */
263     public function __construct(array $_options) 
264     {
265         $this->_options = $_options;
266         $this->_ldap = new Tinebase_Ldap($_options);
267         $this->_ldap->bind();
268         
269         //$this->_baseDn = "ou=contacts,ou=von-und-zu-weiss.de,dc=d80-237-148-76";
270         $this->baseDn = $this->_options['userDn'];
271         
272         $this->_checkSchemas();
273         
274     }
275     
276     /**
277      * Search for records matching given filter
278      *
279      * @param  Tinebase_Record_Interface  $_filter
280      * @param  Tinebase_Model_Pagination $_pagination
281      * @return Tinebase_Record_RecordSet
282      */
283     public function search(Tinebase_Record_Interface $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL)
284     {
285         
286     }
287     
288     /**
289      * Gets total count of search with $_filter
290      * 
291      * @param Tinebase_Record_Interface $_filter
292      * @return int
293      */
294     public function searchCount(Tinebase_Record_Interface $_filter)
295     {
296         
297     }
298     
299     /**
300      * returns a ldap filter string
301      *
302      * @param  Tinebase_Record_Abstract $_filter
303      * @return String
304      */
305     protected function _getFilter(Tinebase_Record_Abstract $_filter) {
306         
307     }
308     
309     /**
310      * Return a single record
311      *
312      * @param string $_id uuid / uidnumber ???
313      * @return Tinebase_Record_Interface
314      */
315     public function get($_id)
316     {
317         $contactData = $this->_ldap->fetch($this->_baseDn, "entryuuid=$_id", $this->_getSupportedLdapAttributes());
318         
319         if (! $contactData) {
320             throw new Addressbook_Exception_NotFound("Contact with id $_id not found.");
321         }
322         $contact = $this->_ldap2Contacts(array($contactData))->offsetGet(0);
323         $contact->jpegphoto = $this->_ldap->fetchBinaryAttribute($this->_baseDn, "entryuuid=$_id", 'jpegphoto');
324         
325         return $contact;
326     }
327     
328     /**
329      * Returns a set of contacts identified by their id's
330      * 
331      * @param  string|array $_id Ids
332      * @param array $_containerIds all allowed container ids that are added to getMultiple query [not used]
333      * @return Tinebase_RecordSet of Tinebase_Record_Interface
334      */
335     public function getMultiple($_ids, $_containerIds = NULL)
336     {
337         $ids = is_array($_ids) ? $_ids : (array) $_ids;
338         
339         $idFilter = '';
340         foreach ($ids as $id) {
341             $idFilter .= "(entryuuid=$id)";
342         }
343         $filter = "(&(objectclass=inetorgperson)(|$idFilter))";
344         
345         $rawLdapData = $this->_ldap->fetchAll($this->_baseDn, $filter, $this->_getSupportedLdapAttributes());
346         
347         $contacts = $this->_ldap2Contacts($rawLdapData);
348         
349         return $contacts;
350     }
351
352     /**
353      * Gets all entries
354      *
355      * @param string $_orderBy Order result by
356      * @param string $_orderDirection Order direction - allowed are ASC and DESC
357      * @throws Tinebase_Exception_InvalidArgument
358      * @return Tinebase_Record_RecordSet
359      */
360     public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
361     {
362         if(! in_array($_orderBy, $this->_getSupportedRecordFields())) {
363             throw new Tinebase_Exception_InvalidArgument('$_orderBy field "'. $_orderBy . '" is not supported by this backend instance');
364         }
365         
366         $rawLdapData = $this->_ldap->fetchAll($this->_baseDn, 'objectclass=inetorgperson', $this->_getSupportedLdapAttributes());
367         
368         $contacts = $this->_ldap2Contacts($rawLdapData);
369         
370         $contacts->sort($_orderBy, $_orderDirection);
371
372         return $contacts;
373     }
374     
375     /**
376      * Create a new persistent contact
377      *
378      * @param  Tinebase_Record_Interface $_record
379      * @return Tinebase_Record_Interface
380      */
381     public function create(Tinebase_Record_Interface $_record)
382     {
383         
384     }
385     
386     /**
387      * Upates an existing persistent record
388      *
389      * @param  Tinebase_Record_Interface $_contact
390      * @return Tinebase_Record_Interface
391      */
392     public function update(Tinebase_Record_Interface $_record)
393     {
394         
395     }
396     
397     /**
398      * Deletes one or more existing persistent record(s)
399      *
400      * @param string|array $_identifier
401      * @return void
402      */
403     public function delete($_identifier)
404     {
405         
406     }
407     
408     /**
409      * fetch one contact of a user identified by his user_id
410      *
411      * @param   int $_userId
412      * @return  Addressbook_Model_Contact 
413      * @throws  Addressbook_Exception_NotFound if contact not found
414      */
415     public function getByUserId($_userId)
416     {
417         $userId = Tinebase_Model_User::convertUserIdToInt($_userId);
418         
419         $contactData = $this->_ldap->fetch($this->_baseDn, "uidnumber=$userId", $this->_getSupportedLdapAttributes());
420         
421         if (! $contactData) {
422             throw new Addressbook_Exception_NotFound("Contact with user id $_userId not found.");
423         }
424         $contact = $this->_ldap2Contacts(array($contactData))->offsetGet(0);
425         $contact->jpegphoto = $this->_ldap->fetchBinaryAttribute($this->_baseDn, "uidnumber=$userId", 'jpegphoto');
426         
427         return $contact;
428     }
429     
430     /**
431      * get backend type
432      *
433      * @return string
434      */
435     public function getType()
436     {
437         return self::TYPE;
438     }
439     
440     /**
441      * returns a record set of Addressbook_Model_Contacts filled from raw ldap data
442      * 
443      * @param  array $_data raw ldap contacts data
444      * @return Tinebase_Record_RecordSet of Addressbook_Model_Contacts
445      */
446     protected function _ldap2Contacts($_data)
447     {
448         $contactsArray = array();
449         
450         foreach ($_data as $ldapEntry) {
451             $contactArray = $this->_mapLdap2Contact($ldapEntry);
452             array_push($contactsArray, $contactArray);
453         }
454         
455         $contacts = new Tinebase_Record_RecordSet('Addressbook_Model_Contact', $contactsArray, true, self::LDAPDATEFORMAT);
456         
457         // dn to userids -> later lets just unset this data for the moment
458         $contacts->created_by = '';
459         $contacts->last_modified_by = '';
460         
461         return $contacts;
462     }
463     
464     /**
465      * maps data of raw ldapEntry to fields of a contact record
466      * 
467      * @param array natvie ldap data of an entry
468      * @return array raw contact data
469      */
470     protected function _mapLdap2Contact($_data)
471     {
472         $contactArray = array();
473         $schemaMap = array();
474         
475         // look for each supported record filed if we find a value in the data
476         foreach ($this->_getSupportedRecordFields() as $field) {
477             foreach ($this->_attributesMaps as $schemaName => $mapping) {
478                 if(in_array($schemaName, $this->_availableSchemas)) {
479                     if ((isset($mapping[$field]) || array_key_exists($field, $mapping))) {
480                         
481                         $attributeName = $mapping[$field];
482                         if ((isset($_data[$attributeName]) || array_key_exists($attributeName, $_data)) && $_data[$attributeName]['count'] > 0) {
483                             // heureka! we found a value for the current field.
484                             // Lets take it and search for the next field.
485                             $schemaMap[$field] = $schemaName;
486                             if ($_data[$attributeName]['count'] == 1) {
487                                 $contactArray[$field] = $_data[$attributeName][0];
488                             } else {
489                                 unset($_data[$attributeName]['count']);
490                                 $contactArray[$field] = $_data[$attributeName];
491                             }
492                             break;
493                         }
494                     }
495                 }
496             }
497         }
498         
499         $this->_transformLdap2Contact($contactArray, $schemaMap);
500         return $contactArray;
501     }
502     
503     /**
504      * do final transformation from ldap to contact after mapping
505      *
506      * @param array $_contactArray $fieldName => $fieldValue
507      * @param array $_schemaMap $fieldName => $schemaName (origin of value)
508      */
509     protected function _transformLdap2Contact(&$_contactArray, $_schemaMap)
510     {
511         // find out n_prefix/n_suffix and clear n_given (inetorgperson)
512         // adopt country codes (do we need a mapping?)
513     }
514     
515     /**
516      * checks if all required schemas are available
517      * 
518      * @throws Addressbook_Exception_Backend
519      * @return void
520      */
521     protected function _checkSchemas()
522     {
523         $missingSchemas = array_diff($this->_requierdSchemas, $this->_availableSchemas);
524         if (count($missingSchemas) > 0) {
525             throw new Addressbook_Exception_Backend("missing required schemas: " . print_r($missingSchemas, true));
526         }
527     }
528     
529     /**
530      * returns supported contact attributes of the ldap entry
531      *
532      * @return array of ldap attributes 
533      */
534     protected function _getSupportedLdapAttributes()
535     {
536         if (! $this->_supportedLdapAttributes) {
537             $attributes = array();
538             
539             foreach ($this->_attributesMaps as $schemaName => $mapping) {
540                 if(in_array($schemaName, $this->_availableSchemas)) {
541                     $attributes = array_merge($attributes, array_values($mapping));
542                 }
543             }
544             $this->_supportedLdapAttributes = array_values(array_unique($attributes));
545         }
546         
547         return $this->_supportedLdapAttributes;
548     }
549     
550     /**
551      * returns supported contact record fields
552      *
553      * @return array
554      */
555     public function _getSupportedRecordFields()
556     {
557         if (! $this->_supportedRecordFields) {
558             $fields = array();
559             
560             foreach ($this->_attributesMaps as $schemaName => $mapping) {
561                 if(in_array($schemaName, $this->_availableSchemas)) {
562                     $fields = array_merge($fields, array_keys($mapping));
563                 }
564             }
565             $this->_supportedRecordFields = array_values(array_unique($fields));
566         }
567         
568         return $this->_supportedRecordFields;
569     }
570 }