4 * contacts ldap 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)
15 * contacts ldap backend
17 * NOTE: LDAP charset is allways UTF-8 (RFC2253) so we don't have to cope with
18 * charset conversions here ;-)
20 * @package Addressbook
23 class Addressbook_Backend_Ldap implements Tinebase_Backend_Interface
26 * backend type constant
31 * date representation used by ldap
33 const LDAPDATEFORMAT = 'YmdHis';
36 * ldap directory connection
40 protected $_ldap = NULL;
47 protected $_baseDn = NULL;
51 * @see Zend_Ldap options + userDn + groupDn
55 protected $_options = NULL;
58 * list of reqired schemas
62 protected $_requierdSchemas = array(
69 * list of available schemas in ldap server
71 * @todo get this list from $this->_ldap
74 protected $_availableSchemas = array(
78 'mozillaabpersonalpha',
83 * list of supportetd tine contact record fields
85 * NOTE: this list depends on the supportet schemas by current ldap server
86 * @see $this->__attributesMaps
90 protected $_supportedRecordFields = NULL;
93 * list of supported attributes by current ldap server
95 * NOTE: dynamically determined by supported schemas
99 protected $_supportedLdapAttributes = NULL;
102 * attributes mapping for supported schemas
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!
108 * @var array schemaName => array (recordField => ldapAttribute)
110 protected $_attributesMaps = array(
112 * Abstraction of an account with POSIX attributes
113 * NOTE: For contacts we only use the reference to the account
115 'posixaccount' => array(
116 'account_id' => 'uidnumber',
120 * generic openldap attributes
124 'created_by' => 'creatorsname',
125 'creation_time' => 'createtimestamp',
126 'last_modified_by' => 'modifiersname',
127 'last_modified_time' => 'modifytimestamp',
131 * RFC2798: Internet Organizational Person
133 'inetorgperson' => array(
135 'n_given' => 'givenname',
138 'note' => 'description',
139 'url' => 'labeleduri',
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',
153 'room' => 'roomnumber',
154 'jpegphoto' => 'jpegphoto',
155 'n_fileas' => 'displayname',
156 'label' => 'postaladdress',
157 'pubkey' => 'usersmimecertificate',
161 * Mozilla LDAP Address Book Schema (alpha)
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
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'
186 //'' => 'postOfficeBox'
190 * Mozilla LDAP Address Book Schema
191 * similar to the newer mozillaAbPerson, but uses mozillaPostalAddress2 instead of mozillaStreet2
194 * @link https://bugzilla.mozilla.org/attachment.cgi?id=104858&action=view
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',
211 * Objectclass geared to Evolution Usage
213 * @link http://projects.gnome.org/evolution/index.shtml
215 'evolutionperson' => array(
216 'bday' => 'birthdate',
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'
233 //'' => 'anniversary'
235 //'' => 'companyPhone'
236 //'' => 'otherFacsimileTelephoneNumber'
240 //'' => 'categories' //(deprecated)
244 * unsupported tine fields:
246 //'is_deleted' => '',
247 //'deleted_time' => '',
248 //'deleted_by' => '',
250 //'container_id' => '',
251 //'salutation' => '',
259 * constructs a contacts ldap backend
261 * @param array $options Options used in connecting, binding, etc.
263 public function __construct(array $_options)
265 $this->_options = $_options;
266 $this->_ldap = new Tinebase_Ldap($_options);
267 $this->_ldap->bind();
269 //$this->_baseDn = "ou=contacts,ou=von-und-zu-weiss.de,dc=d80-237-148-76";
270 $this->baseDn = $this->_options['userDn'];
272 $this->_checkSchemas();
277 * Search for records matching given filter
279 * @param Tinebase_Record_Interface $_filter
280 * @param Tinebase_Model_Pagination $_pagination
281 * @return Tinebase_Record_RecordSet
283 public function search(Tinebase_Record_Interface $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL)
289 * Gets total count of search with $_filter
291 * @param Tinebase_Record_Interface $_filter
294 public function searchCount(Tinebase_Record_Interface $_filter)
300 * returns a ldap filter string
302 * @param Tinebase_Record_Abstract $_filter
305 protected function _getFilter(Tinebase_Record_Abstract $_filter) {
310 * Return a single record
312 * @param string $_id uuid / uidnumber ???
313 * @return Tinebase_Record_Interface
315 public function get($_id)
317 $contactData = $this->_ldap->fetch($this->_baseDn, "entryuuid=$_id", $this->_getSupportedLdapAttributes());
319 if (! $contactData) {
320 throw new Addressbook_Exception_NotFound("Contact with id $_id not found.");
322 $contact = $this->_ldap2Contacts(array($contactData))->offsetGet(0);
323 $contact->jpegphoto = $this->_ldap->fetchBinaryAttribute($this->_baseDn, "entryuuid=$_id", 'jpegphoto');
329 * Returns a set of contacts identified by their id's
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
335 public function getMultiple($_ids, $_containerIds = NULL)
337 $ids = is_array($_ids) ? $_ids : (array) $_ids;
340 foreach ($ids as $id) {
341 $idFilter .= "(entryuuid=$id)";
343 $filter = "(&(objectclass=inetorgperson)(|$idFilter))";
345 $rawLdapData = $this->_ldap->fetchAll($this->_baseDn, $filter, $this->_getSupportedLdapAttributes());
347 $contacts = $this->_ldap2Contacts($rawLdapData);
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
360 public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
362 if(! in_array($_orderBy, $this->_getSupportedRecordFields())) {
363 throw new Tinebase_Exception_InvalidArgument('$_orderBy field "'. $_orderBy . '" is not supported by this backend instance');
366 $rawLdapData = $this->_ldap->fetchAll($this->_baseDn, 'objectclass=inetorgperson', $this->_getSupportedLdapAttributes());
368 $contacts = $this->_ldap2Contacts($rawLdapData);
370 $contacts->sort($_orderBy, $_orderDirection);
376 * Create a new persistent contact
378 * @param Tinebase_Record_Interface $_record
379 * @return Tinebase_Record_Interface
381 public function create(Tinebase_Record_Interface $_record)
387 * Upates an existing persistent record
389 * @param Tinebase_Record_Interface $_contact
390 * @return Tinebase_Record_Interface
392 public function update(Tinebase_Record_Interface $_record)
398 * Deletes one or more existing persistent record(s)
400 * @param string|array $_identifier
403 public function delete($_identifier)
409 * fetch one contact of a user identified by his user_id
411 * @param int $_userId
412 * @return Addressbook_Model_Contact
413 * @throws Addressbook_Exception_NotFound if contact not found
415 public function getByUserId($_userId)
417 $userId = Tinebase_Model_User::convertUserIdToInt($_userId);
419 $contactData = $this->_ldap->fetch($this->_baseDn, "uidnumber=$userId", $this->_getSupportedLdapAttributes());
421 if (! $contactData) {
422 throw new Addressbook_Exception_NotFound("Contact with user id $_userId not found.");
424 $contact = $this->_ldap2Contacts(array($contactData))->offsetGet(0);
425 $contact->jpegphoto = $this->_ldap->fetchBinaryAttribute($this->_baseDn, "uidnumber=$userId", 'jpegphoto');
435 public function getType()
441 * returns a record set of Addressbook_Model_Contacts filled from raw ldap data
443 * @param array $_data raw ldap contacts data
444 * @return Tinebase_Record_RecordSet of Addressbook_Model_Contacts
446 protected function _ldap2Contacts($_data)
448 $contactsArray = array();
450 foreach ($_data as $ldapEntry) {
451 $contactArray = $this->_mapLdap2Contact($ldapEntry);
452 array_push($contactsArray, $contactArray);
455 $contacts = new Tinebase_Record_RecordSet('Addressbook_Model_Contact', $contactsArray, true, self::LDAPDATEFORMAT);
457 // dn to userids -> later lets just unset this data for the moment
458 $contacts->created_by = '';
459 $contacts->last_modified_by = '';
465 * maps data of raw ldapEntry to fields of a contact record
467 * @param array natvie ldap data of an entry
468 * @return array raw contact data
470 protected function _mapLdap2Contact($_data)
472 $contactArray = array();
473 $schemaMap = array();
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))) {
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];
489 unset($_data[$attributeName]['count']);
490 $contactArray[$field] = $_data[$attributeName];
499 $this->_transformLdap2Contact($contactArray, $schemaMap);
500 return $contactArray;
504 * do final transformation from ldap to contact after mapping
506 * @param array $_contactArray $fieldName => $fieldValue
507 * @param array $_schemaMap $fieldName => $schemaName (origin of value)
509 protected function _transformLdap2Contact(&$_contactArray, $_schemaMap)
511 // find out n_prefix/n_suffix and clear n_given (inetorgperson)
512 // adopt country codes (do we need a mapping?)
516 * checks if all required schemas are available
518 * @throws Addressbook_Exception_Backend
521 protected function _checkSchemas()
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));
530 * returns supported contact attributes of the ldap entry
532 * @return array of ldap attributes
534 protected function _getSupportedLdapAttributes()
536 if (! $this->_supportedLdapAttributes) {
537 $attributes = array();
539 foreach ($this->_attributesMaps as $schemaName => $mapping) {
540 if(in_array($schemaName, $this->_availableSchemas)) {
541 $attributes = array_merge($attributes, array_values($mapping));
544 $this->_supportedLdapAttributes = array_values(array_unique($attributes));
547 return $this->_supportedLdapAttributes;
551 * returns supported contact record fields
555 public function _getSupportedRecordFields()
557 if (! $this->_supportedRecordFields) {
560 foreach ($this->_attributesMaps as $schemaName => $mapping) {
561 if(in_array($schemaName, $this->_availableSchemas)) {
562 $fields = array_merge($fields, array_keys($mapping));
565 $this->_supportedRecordFields = array_values(array_unique($fields));
568 return $this->_supportedRecordFields;