1<?php
2/**
3 * EGroupware API: Contacts LDAP Backend
4 *
5 * @link http://www.egroupware.org
6 * @author Cornelius Weiss <egw-AT-von-und-zu-weiss.de>
7 * @author Lars Kneschke <l.kneschke-AT-metaways.de>
8 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
9 * @package api
10 * @subpackage contacts
11 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
12 * @version $Id$
13 */
14
15namespace EGroupware\Api\Contacts;
16
17use EGroupware\Api;
18
19/**
20 * LDAP Backend for contacts, compatible with vars and parameters of eTemplate's so_sql.
21 * Maybe one day this becomes a generalized ldap storage object :-)
22 *
23 * All values used to construct filters need to run through Api\Ldap::quote(),
24 * to be save against LDAP query injection!!!
25 */
26class Ldap
27{
28
29	const ALL = 0;
30	const ACCOUNTS = 1;
31	const PERSONAL = 2;
32	const GROUP = 3;
33
34	var $data;
35
36	/**
37	 * internal name of the id, gets mapped to uid
38	 *
39	 * @var string
40	 */
41	var $contacts_id='id';
42
43	/**
44	* @var string $accountName holds the accountname of the current user
45	*/
46	var $accountName;
47
48	/**
49	* @var object $ldapServerInfo holds the information about the current used ldap server
50	*/
51	var $ldapServerInfo;
52
53	/**
54	* @var int $ldapLimit how many rows to fetch from ldap server
55	*/
56	var $ldapLimit = 2000;
57
58	/**
59	* @var string $personalContactsDN holds the base DN for the personal addressbooks
60	*/
61	var $personalContactsDN;
62
63	/**
64	* @var string $sharedContactsDN holds the base DN for the shared addressbooks
65	*/
66	var $sharedContactsDN;
67
68	/**
69	* @var string $accountContactsDN holds the base DN for accounts addressbook
70	*/
71	var $accountContactsDN;
72
73	/**
74	 * Filter used for accounts addressbook
75	 * @var string
76	 */
77	var $accountsFilter = '(objectclass=posixaccount)';
78
79	/**
80	 * Filter used for all addressbooks
81	 * @var string
82	 */
83	var $contactsFilter = '(objectclass=inetorgperson)';
84
85	/**
86	* @var string $allContactsDN holds the base DN of all addressbook
87	*/
88	var $allContactsDN;
89
90	/**
91	 * Attribute used for DN
92	 *
93	 * @var string
94	 */
95	var $dn_attribute='uid';
96
97	/**
98	 * Do NOT attempt to change DN (dn-attribute can NOT be part of schemas used in addressbook!)
99	 *
100	 * @var boolean
101	 */
102	var $never_change_dn = false;
103
104	/**
105	* @var int $total holds the total count of found rows
106	*/
107	var $total;
108
109	/**
110	 * Charset used by eGW
111	 *
112	 * @var string
113	 */
114	var $charset;
115
116	/**
117	 * LDAP searches only a limited set of attributes for performance reasons,
118	 * you NEED an index for that columns, ToDo: make it configurable
119	 * minimum: $this->columns_to_search = array('n_family','n_given','org_name','email');
120	 */
121	var $search_attributes = array(
122		'n_family','n_middle','n_given','org_name','org_unit',
123		'adr_one_locality','adr_two_locality','note',
124		'email','mozillasecondemail','uidnumber',
125	);
126
127	/**
128	 * maps between diverse ldap schema and the eGW internal names
129	 *
130	 * The ldap attribute names have to be lowercase!!!
131	 *
132	 * @var array
133	 */
134	var $schema2egw = array(
135		'posixaccount' => array(
136			'account_id'	=> 'uidnumber',
137			'shadowexpire',
138		),
139		'inetorgperson' => array(
140			'n_fn'			=> 'cn',
141			'n_given'		=> 'givenname',
142			'n_family'		=> 'sn',
143			'sound'			=> 'audio',
144			'note'			=> 'description',
145			'url'			=> 'labeleduri',
146			'org_name'		=> 'o',
147			'org_unit'		=> 'ou',
148			'title'			=> 'title',
149			'adr_one_street'		=> 'street',
150			'adr_one_locality'		=> 'l',
151			'adr_one_region'		=> 'st',
152			'adr_one_postalcode'	=> 'postalcode',
153			'tel_work'		=> 'telephonenumber',
154			'tel_home'		=> 'homephone',
155			'tel_fax'		=> 'facsimiletelephonenumber',
156			'tel_cell'		=> 'mobile',
157			'tel_pager'		=> 'pager',
158			'email'			=> 'mail',
159			'room'			=> 'roomnumber',
160			'jpegphoto'		=> 'jpegphoto',
161			'n_fileas'		=> 'displayname',
162			'label'			=> 'postaladdress',
163			'pubkey'		=> 'usersmimecertificate',
164			'uid'			=> 'entryuuid',
165			'id'			=> 'uid',
166		),
167		'organizantionalperson' => [	// ActiveDirectory
168			'n_fn'			=> 'displayname',	// to leave CN as part of DN untouched
169			'n_given'		=> 'givenname',
170			'n_family'		=> 'sn',
171			//'sound'			=> 'audio',
172			'note'			=> 'description',
173			'url'			=> 'url',
174			'org_name'		=> 'company',
175			'org_unit'		=> 'department',
176			'title'			=> 'title',
177			'adr_one_street'		=> 'streetaddress',
178			'adr_one_locality'		=> 'l',
179			'adr_one_region'		=> 'st',
180			'adr_one_postalcode'	=> 'postalcode',
181			'adr_one_countryname'	=> 'co',
182			'adr_one_countrycode'	=> 'c',
183			'tel_work'		=> 'telephonenumber',
184			'tel_home'		=> 'homephone',
185			'tel_fax'		=> 'facsimiletelephonenumber',
186			'tel_cell'		=> 'mobile',
187			'tel_pager'		=> 'pager',
188			'tel_other'		=> 'othertelephone',
189			'tel_cell_private' => 'othermobile',
190			'assistent'		=> 'assistant',
191			'email'			=> 'mail',
192			'room'			=> 'roomnumber',
193			'jpegphoto'		=> 'jpegphoto',
194			'n_fileas'		=> 'displayname',
195			'label'			=> 'postaladdress',
196			'pubkey'		=> 'usersmimecertificate',
197			'uid'			=> 'objectguid',
198			'id'			=> 'objectguid',
199		],
200		#displayName
201		#mozillaCustom1
202		#mozillaCustom2
203		#mozillaCustom3
204		#mozillaCustom4
205		#mozillaHomeUrl
206		#mozillaNickname
207		#mozillaUseHtmlMail
208		#nsAIMid
209		#postOfficeBox
210		'mozillaabpersonalpha' => array(
211			'adr_one_street2'	=> 'mozillaworkstreet2',
212			'adr_one_countryname'	=> 'c',	// 2 letter country code
213			'adr_one_countrycode'	=> 'c',	// 2 letter country code
214			'adr_two_street'	=> 'mozillahomestreet',
215			'adr_two_street2'	=> 'mozillahomestreet2',
216			'adr_two_locality'	=> 'mozillahomelocalityname',
217			'adr_two_region'	=> 'mozillahomestate',
218			'adr_two_postalcode'	=> 'mozillahomepostalcode',
219			'adr_two_countryname'	=> 'mozillahomecountryname',
220			'adr_two_countrycode'	=> 'mozillahomecountryname',
221			'email_home'		=> 'mozillasecondemail',
222			'url_home'			=> 'mozillahomeurl',
223		),
224		// similar to the newer mozillaAbPerson, but uses mozillaPostalAddress2 instead of mozillaStreet2
225		'mozillaorgperson' => array(
226			'adr_one_street2'	=> 'mozillapostaladdress2',
227			'adr_one_countrycode'	=> 'c',	// 2 letter country code
228			'adr_one_countryname'	=> 'co',	// human readable country name, must be after 'c' to take precedence on read!
229			'adr_two_street'	=> 'mozillahomestreet',
230			'adr_two_street2'	=> 'mozillahomepostaladdress2',
231			'adr_two_locality'	=> 'mozillahomelocalityname',
232			'adr_two_region'	=> 'mozillahomestate',
233			'adr_two_postalcode'	=> 'mozillahomepostalcode',
234			'adr_two_countryname'	=> 'mozillahomecountryname',
235			'email_home'		=> 'mozillasecondemail',
236			'url_home'			=> 'mozillahomeurl',
237		),
238		# managerName
239		# otherPostalAddress
240		# mailer
241		# anniversary
242		# spouseName
243		# companyPhone
244		# otherFacsimileTelephoneNumber
245		# radio
246		# telex
247		# tty
248		# categories(deprecated)
249		'evolutionperson' => array(
250			'bday'			=> 'birthdate',
251			'note'			=> 'note',
252			'tel_car'		=> 'carphone',
253			'tel_prefer'	=> 'primaryphone',
254			'cat_id'		=> 'category',	// special handling in _egw2evolutionperson method
255			'role'			=> 'businessrole',
256			'tel_assistent'	=> 'assistantphone',
257			'assistent'		=> 'assistantname',
258			'n_fileas'		=> 'fileas',
259			'tel_fax_home'	=> 'homefacsimiletelephonenumber',
260			'freebusy_uri'	=> 'freeBusyuri',
261			'calendar_uri'	=> 'calendaruri',
262			'tel_other'		=> 'otherphone',
263			'tel_cell_private' => 'callbackphone',	// not the best choice, but better then nothing
264		),
265		// additional schema can be added here, including special functions
266
267		/**
268		 * still unsupported fields in LDAP:
269		 * --------------------------------
270		 * tz
271		 * geo
272		 */
273	);
274
275	/**
276	 * additional schema required by one of the above schema
277	 *
278	 * @var array
279	 */
280	var $required_subs = array(
281		'inetorgperson' => array('person'),
282	);
283
284	/**
285	 * array with the names of all ldap attributes of the above schema2egw array
286	 *
287	 * @var array
288	 */
289	var  $all_attributes = array();
290
291	/**
292	 * LDAP configuration
293	 *
294	 * @var array values for keys "ldap_contact_context", "ldap_host", "ldap_context"
295	 */
296	private $ldap_config;
297
298	/**
299	 * LDAP connection
300	 *
301	 * @var resource
302	 */
303	var $ds;
304
305	/**
306	 * constructor of the class
307	 *
308	 * @param array $ldap_config =null default use from $GLOBALS['egw_info']['server']
309	 * @param resource $ds =null ldap connection to use
310	 */
311	function __construct(array $ldap_config=null, $ds=null)
312	{
313		//$this->db_data_cols 	= $this->stock_contact_fields + $this->non_contact_fields;
314		$this->accountName 		= $GLOBALS['egw_info']['user']['account_lid'];
315
316		if ($ldap_config)
317		{
318			$this->ldap_config = $ldap_config;
319		}
320		else
321		{
322			$this->ldap_config =& $GLOBALS['egw_info']['server'];
323		}
324		$this->accountContactsDN	= $this->ldap_config['ldap_context'];
325		$this->allContactsDN		= $this->ldap_config['ldap_contact_context'];
326		$this->personalContactsDN	= 'ou=personal,ou=contacts,'. $this->allContactsDN;
327		$this->sharedContactsDN		= 'ou=shared,ou=contacts,'. $this->allContactsDN;
328
329		if ($ds)
330		{
331			$this->ds = $ds;
332		}
333		else
334		{
335			$this->connect();
336		}
337		$this->ldapServerInfo = $GLOBALS['egw']->ldap->getLDAPServerInfo($this->ldap_config['ldap_contact_host']);
338
339		foreach($this->schema2egw as $attributes)
340		{
341			$this->all_attributes = array_merge($this->all_attributes,array_values($attributes));
342		}
343		$this->all_attributes = array_values(array_unique($this->all_attributes));
344
345		$this->charset = Api\Translation::charset();
346
347		// add ldap_search_filter from admin
348		$accounts_filter = str_replace(['%user','%domain'], ['*', $GLOBALS['egw_info']['user']['domain']],
349			$this->ldap_config['ldap_search_filter'] ?: '(uid=%user)');
350		$this->accountsFilter = "(&$this->accountsFilter$accounts_filter)";
351	}
352
353	/**
354	 * __wakeup function gets called by php while unserializing the object to reconnect with the ldap server
355	 */
356	function __wakeup()
357	{
358		$this->connect();
359	}
360
361	/**
362	 * connect to LDAP server
363	 *
364	 * @param boolean $admin =false true (re-)connect with admin not user credentials, eg. to modify accounts
365	 */
366	function connect($admin = false)
367	{
368		if ($admin)
369		{
370			$this->ds = Api\Ldap::factory();
371		}
372		// if ldap is NOT the contact repository, we only do accounts and need to use the account-data
373		elseif (substr($GLOBALS['egw_info']['server']['contact_repository'],-4) != 'ldap')	// not (ldap or sql-ldap)
374		{
375			$this->ldap_config['ldap_contact_host'] = $this->ldap_config['ldap_host'];
376			$this->allContactsDN = $this->ldap_config['ldap_context'];
377			$this->ds = Api\Ldap::factory();
378		}
379		else
380		{
381			$this->ds = Api\Ldap::factory(true,
382				$this->ldap_config['ldap_contact_host'],
383				$GLOBALS['egw_info']['user']['account_dn'],
384				$GLOBALS['egw_info']['user']['passwd']
385			);
386		}
387	}
388
389	/**
390	 * Returns the supported fields of this LDAP server (based on the objectclasses it supports)
391	 *
392	 * @return array with eGW contact field names
393	 */
394	function supported_fields()
395	{
396		$fields = array(
397			'id','tid','owner',
398			'n_middle','n_prefix','n_suffix',	// stored in the cn
399			'created','modified',				// automatic timestamps
400			'creator','modifier',				// automatic for non accounts
401			'private',							// true for personal addressbooks, false otherwise
402		);
403		foreach($this->schema2egw as $objectclass => $mapping)
404		{
405			if($this->ldapServerInfo->supportsObjectClass($objectclass))
406			{
407				$fields = array_merge($fields,array_keys($mapping));
408			}
409		}
410		return array_values(array_unique($fields));
411	}
412
413	/**
414	 * Return LDAP filter for contact id
415	 *
416	 * @param string $id
417	 * @return string
418	 */
419	protected function id_filter($id)
420	{
421		return '(|(entryUUID='.Api\Ldap::quote($id).')(uid='.Api\Ldap::quote($id).'))';
422	}
423
424	/**
425	 * Return LDAP filter for (multiple) contact ids
426	 *
427	 * @param array|string $ids
428	 * @throws Api\Exception\AssertionFailed if $contact_id is no valid GUID (for ADS!)
429	 * @return string
430	 */
431	protected function ids_filter($ids)
432	{
433		if (!is_array($ids) || count($ids) == 1)
434		{
435			return $this->id_filter(is_array($ids) ? array_shift($ids) : $ids);
436		}
437		$filter = array();
438		foreach($ids as $id)
439		{
440			$filter[] = $this->id_filter($id);
441		}
442		return '(|'.implode('', $filter).')';
443	}
444
445	/**
446	 * reads contact data
447	 *
448	 * @param string|array $contact_id contact_id or array with values for id or account_id
449	 * @return array/boolean data if row could be retrived else False
450	*/
451	function read($contact_id)
452	{
453		if (is_array($contact_id) && isset($contact_id['account_id']) ||
454			!is_array($contact_id) && substr($contact_id,0,8) == 'account:')
455		{
456			$filter = 'uidNumber='.(int)(is_array($contact_id) ? $contact_id['account_id'] : substr($contact_id,8));
457		}
458		else
459		{
460			if (is_array($contact_id)) $contact_id = isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid'];
461			$filter = $this->id_filter($contact_id);
462		}
463		$rows = $this->_searchLDAP($this->allContactsDN,
464			$filter, $this->all_attributes, self::ALL, array('_posixaccount2egw'));
465
466		return $rows ? $rows[0] : false;
467	}
468
469	/**
470	 * Remove attributes we are not allowed to update
471	 *
472	 * @param array $attributes
473	 */
474	function sanitize_update(array &$ldapContact)
475	{
476		// never allow to change the uidNumber (account_id) on update, as it could be misused by eg. xmlrpc or syncml
477		unset($ldapContact['uidnumber']);
478
479		unset($ldapContact['entryuuid']);	// not allowed to modify that, no need either
480
481		unset($ldapContact['objectClass']);
482	}
483
484	/**
485	 * saves the content of data to the db
486	 *
487	 * @param array $keys if given $keys are copied to data before saveing => allows a save as
488	 * @return int 0 on success and errno != 0 else
489	 */
490	function save($keys=null)
491	{
492		//error_log(__METHOD__."(".array2string($keys).") this->data=".array2string($this->data));
493		if(is_array($keys))
494		{
495			$this->data = is_array($this->data) ? array_merge($this->data,$keys) : $keys;
496		}
497
498		$data =& $this->data;
499		$isUpdate = false;
500		$ldapContact = array();
501
502		// generate addressbook dn
503		if((int)$data['owner'])
504		{
505			// group address book
506			if(!($cn = strtolower($GLOBALS['egw']->accounts->id2name((int)$data['owner']))))
507			{
508				error_log('Unknown owner');
509				return true;
510			}
511			$baseDN = 'cn='. $cn .','.($data['owner'] < 0 ? $this->sharedContactsDN : $this->personalContactsDN);
512		}
513		// only an admin or the user itself is allowed to change the data of an account
514		elseif ($data['account_id'] && ($GLOBALS['egw_info']['user']['apps']['admin'] ||
515			$data['account_id'] == $GLOBALS['egw_info']['user']['account_id']))
516		{
517			// account
518			$baseDN = $this->accountContactsDN;
519			$cn	= false;
520			// we need an admin connection
521			$this->connect(true);
522
523			// for sql-ldap we need to account_lid/uid as id, NOT the contact_id in id!
524			if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap')
525			{
526				$data['id'] = $GLOBALS['egw']->accounts->id2name($data['account_id']);
527			}
528		}
529		else
530		{
531			error_log("Permission denied, to write: data[owner]=$data[owner], data[account_id]=$data[account_id], account_id=".$GLOBALS['egw_info']['user']['account_id']);
532			return lang('Permission denied !!!');	// only admin or the user itself is allowd to write accounts!
533		}
534		// check if $baseDN exists. If not create it
535		if (($err = $this->_check_create_dn($baseDN)))
536		{
537			return $err;
538		}
539		// check the existing objectclasses of an entry, none = array() for new ones
540		$oldObjectclasses = array();
541		$attributes = array('dn','cn','objectClass',$this->dn_attribute,'mail');
542
543		$contactUID	= $this->data[$this->contacts_id];
544		if (!empty($contactUID) &&
545			($result = ldap_search($this->ds, $base=$this->allContactsDN, $this->id_filter($contactUID), $attributes)) &&
546			($oldContactInfo = ldap_get_entries($this->ds, $result)) && $oldContactInfo['count'])
547		{
548			unset($oldContactInfo[0]['objectclass']['count']);
549			foreach($oldContactInfo[0]['objectclass'] as $objectclass)
550			{
551				$oldObjectclasses[]	= strtolower($objectclass);
552			}
553		   	$isUpdate = true;
554		}
555
556		if(empty($contactUID))
557		{
558			$ldapContact[$this->dn_attribute] = $this->data[$this->contacts_id] = $contactUID = md5(Api\Auth::randomstring(15));
559		}
560		//error_log(__METHOD__."() contactUID='$contactUID', isUpdate=".array2string($isUpdate).", oldContactInfo=".array2string($oldContactInfo));
561		// add for all supported objectclasses the objectclass and it's attributes
562		foreach($this->schema2egw as $objectclass => $mapping)
563		{
564			if(!$this->ldapServerInfo->supportsObjectClass($objectclass)) continue;
565
566			if($objectclass != 'posixaccount' && !in_array($objectclass, $oldObjectclasses))
567			{
568				$ldapContact['objectClass'][] = $objectclass;
569			}
570			if (isset($this->required_subs[$objectclass]))
571			{
572				foreach($this->required_subs[$objectclass] as $sub)
573				{
574					if(!in_array($sub, $oldObjectclasses))
575					{
576						$ldapContact['objectClass'][] = $sub;
577					}
578				}
579			}
580			foreach($mapping as $egwFieldName => $ldapFieldName)
581			{
582				if (is_int($egwFieldName)) continue;
583				if(!empty($data[$egwFieldName]))
584				{
585					// dont convert the (binary) jpegPhoto!
586					$ldapContact[$ldapFieldName] = $ldapFieldName == 'jpegphoto' ? $data[$egwFieldName] :
587						Api\Translation::convert(trim($data[$egwFieldName]),$this->charset,'utf-8');
588				}
589				elseif($isUpdate && isset($data[$egwFieldName]))
590				{
591					$ldapContact[$ldapFieldName] = array();
592				}
593				//error_log(__METHOD__."() ".__LINE__." objectclass=$objectclass, data['$egwFieldName']=".array2string($data[$egwFieldName])." --> ldapContact['$ldapFieldName']=".array2string($ldapContact[$ldapFieldName]));
594			}
595			// handling of special attributes, like cat_id in evolutionPerson
596			$egw2objectclass = '_egw2'.$objectclass;
597			if (method_exists($this,$egw2objectclass))
598			{
599				$this->$egw2objectclass($ldapContact,$data,$isUpdate);
600			}
601		}
602		if ($isUpdate)
603		{
604			// make sure multiple email-addresses in the mail attribute "survive"
605			if (isset($ldapContact['mail']) && $oldContactInfo[0]['mail']['count'] > 1)
606			{
607				$mail = $oldContactInfo[0]['mail'];
608				unset($mail['count']);
609				$mail[0] = $ldapContact['mail'];
610				$ldapContact['mail'] = array_values(array_unique($mail));
611			}
612			// update entry
613			$dn = $oldContactInfo[0]['dn'];
614			$needRecreation = false;
615
616			// add missing objectclasses
617			if($ldapContact['objectClass'] && ($missing=array_diff($ldapContact['objectClass'],$oldObjectclasses)))
618			{
619				if (!@ldap_mod_add($this->ds, $dn, array('objectClass' => $ldapContact['objectClass'])))
620				{
621					if(in_array(ldap_errno($this->ds),array(69,20)))
622					{
623						// need to modify structural objectclass
624						$needRecreation = true;
625						//error_log(__METHOD__."() ".__LINE__." could not add objectclasses ".array2string($missing)." --> need to recreate contact");
626					}
627					else
628					{
629						//echo "<p>ldap_mod_add($this->ds,'$dn',array(objectClass =>".print_r($ldapContact['objectClass'],true)."))</p>\n";
630						error_log(__METHOD__.'() '.__LINE__.' update of '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .')');
631						return $this->_error(__LINE__);
632					}
633				}
634			}
635
636			// check if we need to rename the DN or need to recreate the contact
637			$newRDN = $this->dn_attribute.'='. $ldapContact[$this->dn_attribute];
638			$newDN = $newRDN .','. $baseDN;
639			if ($needRecreation)
640			{
641				$result = ldap_read($this->ds, $dn, 'objectclass=*');
642				$entries = ldap_get_entries($this->ds, $result);
643				$oldContact = Api\Ldap::result2array($entries[0]);
644				unset($oldContact['dn']);
645
646				$newContact = $oldContact;
647				$newContact[$this->dn_attribute] = $ldapContact[$this->dn_attribute];
648
649				if(is_array($ldapContact['objectClass']) && count($ldapContact['objectClass']) > 0)
650				{
651					$newContact['objectclass'] = array_unique(array_map('strtolower',	// objectclasses my have different case
652						array_merge($newContact['objectclass'], $ldapContact['objectClass'])));
653				}
654
655				if(!ldap_delete($this->ds, $dn))
656				{
657					error_log(__METHOD__.'() '.__LINE__.' delete of old '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .')');
658					return $this->_error(__LINE__);
659				}
660				if(!@ldap_add($this->ds, $newDN, $newContact))
661				{
662					error_log(__METHOD__.'() '.__LINE__.' re-create contact as '. $newDN .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .') newContact='.json_encode($newContact));
663					// if adding with new objectclass or dn fails, re-add deleted contact
664					@ldap_add($this->ds, $dn, $oldContact);
665					return $this->_error(__LINE__);
666				}
667				$dn = $newDN;
668			}
669			if ($this->never_change_dn)
670			{
671				// do NOT change DN, set by addressbook_ads, as accounts can be stored in different containers
672			}
673			// try renaming entry if content of dn-attribute changed
674			elseif (strtolower($dn) != strtolower($newDN) || $ldapContact[$this->dn_attribute] != $oldContactInfo[$this->dn_attribute][0])
675			{
676				if (@ldap_rename($this->ds, $dn, $newRDN, null, true))
677				{
678					$dn = $newDN;
679				}
680				else
681				{
682					error_log(__METHOD__.'() '.__LINE__." ldap_rename of $dn to $newRDN failed! ".ldap_error($this->ds));
683				}
684			}
685			unset($ldapContact[$this->dn_attribute]);
686
687			$this->sanitize_update($ldapContact);
688
689			if (!@ldap_modify($this->ds, $dn, $ldapContact))
690			{
691				error_log(__METHOD__.'() '.__LINE__.' update of '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .') ldapContact='.json_encode($ldapContact));
692				return $this->_error(__LINE__);
693			}
694		}
695		else
696		{
697			$dn = $this->dn_attribute.'='. $ldapContact[$this->dn_attribute] .','. $baseDN;
698			unset($ldapContact['entryuuid']);	// trying to write it, gives an error
699
700			if (!@ldap_add($this->ds, $dn, $ldapContact))
701			{
702				error_log(__METHOD__.'() '.__LINE__.' add of '. $dn .' failed errorcode: '. ldap_errno($this->ds) .' ('. ldap_error($this->ds) .') ldapContact='.json_encode($ldapContact));
703				return $this->_error(__LINE__);
704			}
705		}
706		return 0;	// Ok, no error
707	}
708
709	/**
710	 * deletes row representing keys in internal data or the supplied $keys if != null
711	 *
712	 * @param array $keys if given array with col => value pairs to characterise the rows to delete
713	 * @return int affected rows, should be 1 if ok, 0 if an error
714	 */
715	function delete($keys=null)
716	{
717		// single entry
718		if($keys[$this->contacts_id]) $keys = array( 0 => $keys);
719
720		if(!is_array($keys))
721		{
722			$keys = array( $keys);
723		}
724
725		$ret = 0;
726
727		$attributes = array('dn');
728
729		foreach($keys as $entry)
730		{
731			$entry = Api\Ldap::quote(is_array($entry) ? $entry['id'] : $entry);
732			if(($result = ldap_search($this->ds, $this->allContactsDN,
733				"(|(entryUUID=$entry)(uid=$entry))", $attributes)))
734			{
735				$contactInfo = ldap_get_entries($this->ds, $result);
736				if(@ldap_delete($this->ds, $contactInfo[0]['dn']))
737				{
738					$ret++;
739				}
740			}
741		}
742		return $ret;
743	}
744
745	/**
746	 * searches db for rows matching searchcriteria
747	 *
748	 * '*' and '?' are replaced with sql-wildcards '%' and '_'
749	 *
750	 * @param array|string $criteria array of key and data cols, OR a SQL query (content for WHERE), fully quoted (!)
751	 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return
752	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
753	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
754	 * @param string $wildcard ='' appended befor and after each criteria
755	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
756	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
757	 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num)
758	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
759	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
760	 *	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
761	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
762	 * @return array of matching rows (the row is an array of the cols) or False
763	 */
764	function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false)
765	{
766		//error_log(__METHOD__."(".array2string($criteria).", ".array2string($only_keys).", '$order_by', ".array2string($extra_cols).", '$wildcard', '$empty', '$op', ".array2string($start).", ".array2string($filter).")");
767		unset($only_keys, $extra_cols, $empty, $join, $need_full_no_count);	// not used, but required by function signature
768
769		if (is_array($filter['owner']))
770		{
771			if (count($filter['owner']) == 1)
772			{
773				$filter['owner'] = array_shift($filter['owner']);
774			}
775			else
776			{
777				// multiple addressbooks happens currently only via CardDAV or eSync
778				// currently we query all contacts and remove not matching ones (not the most efficient way to do it)
779				$owner_filter = $filter['owner'];
780				unset($filter['owner']);
781			}
782		}
783		// search filter for modified date (eg. for CardDAV sync-report)
784		$datefilter = '';
785		static $egw2ldap = array(
786			'created' => 'createtimestamp',
787			'modified' => 'modifytimestamp',
788		);
789		foreach($filter as $key => $value)
790		{
791			$matches = null;
792			if (is_int($key) && preg_match('/^(contact_)?(modified|created)([<=>]+)([0-9]+)$/', $value, $matches))
793			{
794				$append = '';
795				if ($matches[3] == '>')
796				{
797					$matches['3'] = '<=';
798					$datefilter .= '(!';
799					$append = ')';
800				}
801				$datefilter .= '('.$egw2ldap[$matches[2]].$matches[3].self::_ts2ldap($matches[4]).')'.$append;
802			}
803		}
804
805		if((int)$filter['owner'])
806		{
807			if (!($accountName = $GLOBALS['egw']->accounts->id2name($filter['owner']))) return false;
808
809			$searchDN = 'cn='. Api\Ldap::quote(strtolower($accountName)) .',';
810
811			if ($filter['owner'] < 0)
812			{
813				$searchDN .= $this->sharedContactsDN;
814				$addressbookType = self::GROUP;
815			}
816			else
817			{
818				$searchDN .= $this->personalContactsDN;
819				$addressbookType = self::PERSONAL;
820			}
821		}
822		elseif (!isset($filter['owner']))
823		{
824			$searchDN = $this->allContactsDN;
825			$addressbookType = self::ALL;
826		}
827		else
828		{
829			$searchDN = $this->accountContactsDN;
830			$addressbookType = self::ACCOUNTS;
831		}
832		// create the search filter
833		switch($addressbookType)
834		{
835			case self::ACCOUNTS:
836				$objectFilter = $this->accountsFilter;
837				break;
838			default:
839				$objectFilter = $this->contactsFilter;
840				break;
841		}
842		// exclude expired accounts
843		//$shadowExpireNow = floor((time()+date('Z'))/86400);
844		//$objectFilter .= "(|(!(shadowExpire=*))(shadowExpire>=$shadowExpireNow))";
845		// shadowExpire>= does NOT work, as shadow schema only specifies integerMatch and not integerOrderingMatch :-(
846
847		$searchFilter = '';
848		if(is_array($criteria) && count($criteria) > 0)
849		{
850			$wildcard = $wildcard === '%' ? '*' : '';
851			$searchFilter = '';
852			foreach($criteria as $egwSearchKey => $searchValue)
853			{
854				if (in_array($egwSearchKey, array('id','contact_id')))
855				{
856					try {
857						$searchFilter .= $this->ids_filter($searchValue);
858					}
859					// catch and ignore exception caused by id not being a valid GUID
860					catch(Api\Exception\AssertionFailed $e) {
861						unset($e);
862					}
863					continue;
864				}
865				foreach($this->schema2egw as $mapping)
866				{
867					$matches = null;
868					if (preg_match('/^(egw_addressbook\.)?(contact_)?(.*)$/', $egwSearchKey, $matches))
869					{
870						$egwSearchKey = $matches[3];
871					}
872					if(($ldapSearchKey = $mapping[$egwSearchKey]))
873					{
874						foreach((array)$searchValue as $val)
875						{
876							$searchString = Api\Translation::convert($val, $this->charset,'utf-8');
877							$searchFilter .= '('.$ldapSearchKey.'='.$wildcard.Api\Ldap::quote($searchString).$wildcard.')';
878						}
879						break;
880					}
881				}
882			}
883			if($op == 'AND')
884			{
885				$searchFilter = "(&$searchFilter)";
886			}
887			else
888			{
889				$searchFilter = "(|$searchFilter)";
890			}
891		}
892		$colFilter = $this->_colFilter($filter);
893		$ldapFilter = "(&$objectFilter$searchFilter$colFilter$datefilter)";
894		//error_log(__METHOD__."(".array2string($criteria).", ".array2string($only_keys).", '$order_by', ".array2string($extra_cols).", '$wildcard', '$empty', '$op', ".array2string($start).", ".array2string($filter).") --> ldapFilter='$ldapFilter'");
895		if (!($rows = $this->_searchLDAP($searchDN, $ldapFilter, $this->all_attributes, $addressbookType)))
896		{
897			return $rows;
898		}
899		// only return certain owners --> unset not matching ones
900		if ($owner_filter)
901		{
902			foreach($rows as $k => $row)
903			{
904				if (!in_array($row['owner'],$owner_filter))
905				{
906					unset($rows[$k]);
907					--$this->total;
908				}
909			}
910		}
911		if ($order_by)
912		{
913			$order = array();
914			$sort = 'ASC';
915			foreach(explode(',',$order_by) as $o)
916			{
917				if (substr($o,0,8) == 'contact_') $o = substr($o,8);
918				if (substr($o,-4) == ' ASC')
919				{
920					$sort = 'ASC';
921					$order[] = substr($o,0,-4);
922				}
923				elseif (substr($o,-5) == ' DESC')
924				{
925					$sort = 'DESC';
926					$order[] = substr($o,0,-5);
927				}
928				elseif ($o)
929				{
930					$order[] = $o;
931				}
932			}
933			usort($rows, function($a, $b) use ($order, $sort)
934			{
935				foreach($order as $f)
936				{
937					if($sort == 'ASC')
938					{
939						$strc = strcmp($a[$f], $b[$f]);
940					}
941					else
942					{
943						$strc = strcmp($b[$f], $a[$f]);
944					}
945					if ($strc) return $strc;
946				}
947				return 0;
948			});
949		}
950		// if requested ($start !== false) return only limited resultset
951		if (is_array($start))
952		{
953			list($start,$offset) = $start;
954		}
955		if(is_numeric($start) && is_numeric($offset) && $offset >= 0)
956		{
957			return array_slice($rows, $start, $offset);
958		}
959		elseif(is_numeric($start))
960		{
961			return array_slice($rows, $start, $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']);
962		}
963		return $rows;
964	}
965
966	/**
967	 * Process so_sql like filters (at the moment only a subset used by the addressbook UI
968	 *
969	 * @param array $filter col-name => value pairs or just sql strings
970	 * @return string ldap filter
971	 */
972	function _colFilter($filter)
973	{
974		if (!is_array($filter)) return '';
975
976		$filters = '';
977		foreach($filter as $key => $value)
978		{
979			if ($key != 'cat_id' && $key != 'account_id' && !$value) continue;
980
981			switch((string) $key)
982			{
983				case 'owner':	// already handled
984				case 'tid':		// ignored
985					break;
986
987				case 'account_id':
988					if (is_null($value))
989					{
990						$filters .= '(!(uidNumber=*))';
991					}
992					elseif ($value)
993					{
994						if (is_array($value)) $filters .= '(|';
995						foreach((array)$value as $value)
996						{
997							$filters .= '(uidNumber='.(int)$value.')';
998						}
999						if (is_array($value)) $filters .= ')';
1000					}
1001					break;
1002
1003				case 'cat_id':
1004					if (is_null($value))
1005					{
1006						$filters .= '(!(category=*))';
1007					}
1008					elseif((int)$value)
1009					{
1010						if (!is_object($GLOBALS['egw']->categories))
1011						{
1012							$GLOBALS['egw']->categories = new Api\Categories();
1013						}
1014						$cats = $GLOBALS['egw']->categories->return_all_children((int)$value);
1015						if (count($cats) > 1) $filters .= '(|';
1016						foreach($cats as $cat)
1017						{
1018							$catName = Api\Translation::convert(
1019								$GLOBALS['egw']->categories->id2name($cat),$this->charset,'utf-8');
1020							$filters .= '(category='.Api\Ldap::quote($catName).')';
1021						}
1022						if (count($cats) > 1) $filters .= ')';
1023					}
1024					break;
1025
1026				case 'carddav_name':
1027					if (!is_array($value)) $value = array($value);
1028					foreach($value as &$v)
1029					{
1030						$v = basename($v, '.vcf');
1031					}
1032					// fall through
1033				case 'id':
1034				case 'contact_id':
1035					$filters .= $this->ids_filter($value);
1036					break;
1037
1038				case 'list':
1039					$filters .= $this->membershipFilter($value);
1040					break;
1041
1042				default:
1043					$matches = null;
1044					if (!is_int($key))
1045					{
1046						foreach($this->schema2egw as $mapping)
1047						{
1048							if (isset($mapping[$key]))
1049							{
1050								// todo: value = "!''"
1051								$filters .= '('.$mapping[$key].'='.($value === "!''" ? '*' :
1052									Api\Ldap::quote(Api\Translation::convert($value,$this->charset,'utf-8'))).')';
1053								break;
1054							}
1055						}
1056					}
1057					// filter for letter-search
1058					elseif (preg_match("/^([^ ]+) ".preg_quote($GLOBALS['egw']->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE])." '(.*)%'$/",$value,$matches))
1059					{
1060						list(,$name,$value) = $matches;
1061						if (strpos($name,'.') !== false) list(,$name) = explode('.',$name);
1062						foreach($this->schema2egw as $mapping)
1063						{
1064							if (isset($mapping[$name]))
1065							{
1066								$filters .= '('.$mapping[$name].'='.Api\Ldap::quote(
1067									Api\Translation::convert($value,$this->charset,'utf-8')).'*)';
1068								break;
1069							}
1070						}
1071					}
1072					break;
1073			}
1074		}
1075		return $filters;
1076	}
1077
1078	/**
1079	 * Return a LDAP filter by group membership
1080	 *
1081	 * @param int $gid gidNumber (< 0 as used in EGroupware!)
1082	 * @return string filter or '' if $gid not < 0
1083	 */
1084	function membershipFilter($gid)
1085	{
1086		$filter = '';
1087		if ($gid < 0)
1088		{
1089			$filter .= '(|';
1090			// unfortunately we have no group-membership attribute in LDAP, like in AD
1091			foreach($GLOBALS['egw']->accounts->members($gid, true) as $account_id)
1092			{
1093				$filter .= '(uidNumber='.(int)$account_id.')';
1094			}
1095			$filter .= ')';
1096		}
1097		return $filter;
1098	}
1099
1100	/**
1101	 * Perform the actual ldap-search, retrieve and convert all entries
1102	 *
1103	 * Used be read and search
1104	 *
1105	 * @internal
1106	 * @param string $_ldapContext
1107	 * @param string $_filter
1108	 * @param array $_attributes
1109	 * @param int $_addressbooktype
1110	 * @param array $_skipPlugins =null schema-plugins to skip
1111	 * @return array/boolean with eGW contacts or false on error
1112	 */
1113	function _searchLDAP($_ldapContext, $_filter, $_attributes, $_addressbooktype, array $_skipPlugins=null)
1114	{
1115		$this->total = 0;
1116
1117		$_attributes[] = 'entryUUID';
1118		$_attributes[] = 'objectClass';
1119		$_attributes[] = 'createTimestamp';
1120		$_attributes[] = 'modifyTimestamp';
1121		$_attributes[] = 'creatorsName';
1122		$_attributes[] = 'modifiersName';
1123
1124		//error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype)");
1125
1126		if($_addressbooktype == self::ALL || $_ldapContext == $this->allContactsDN)
1127		{
1128			$result = ldap_search($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit);
1129		}
1130		else
1131		{
1132			$result = @ldap_list($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit);
1133		}
1134		if(!$result || !$entries = ldap_get_entries($this->ds, $result)) return array();
1135		//error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype) result of $entries[count]");
1136
1137		$this->total = $entries['count'];
1138		foreach($entries as $i => $entry)
1139		{
1140			if (!is_int($i)) continue;	// eg. count
1141
1142			$contact = array(
1143				'id'  => $entry['uid'][0] ? $entry['uid'][0] : $entry['entryuuid'][0],
1144				'tid' => 'n',	// the type id for the addressbook
1145			);
1146			foreach($entry['objectclass'] as $ii => $objectclass)
1147			{
1148				$objectclass = strtolower($objectclass);
1149				if (!is_int($ii) || !isset($this->schema2egw[$objectclass]))
1150				{
1151					continue;	// eg. count or unsupported objectclass
1152				}
1153				foreach($this->schema2egw[$objectclass] as $egwFieldName => $ldapFieldName)
1154				{
1155					if(!empty($entry[$ldapFieldName][0]) && !is_int($egwFieldName) && !isset($contact[$egwFieldName]))
1156					{
1157						$contact[$egwFieldName] = Api\Translation::convert($entry[$ldapFieldName][0],'utf-8');
1158					}
1159				}
1160				$objectclass2egw = '_'.$objectclass.'2egw';
1161				if (!in_array($objectclass2egw, (array)$_skipPlugins) &&method_exists($this,$objectclass2egw))
1162				{
1163					if (($ret=$this->$objectclass2egw($contact,$entry)) === false)
1164					{
1165						--$this->total;
1166						continue 2;
1167					}
1168				}
1169			}
1170			// read binary jpegphoto only for one result == call by read
1171			if ($this->total == 1 && isset($entry['jpegphoto'][0]))
1172			{
1173				$bin = ldap_get_values_len($this->ds,ldap_first_entry($this->ds,$result),'jpegphoto');
1174				$contact['jpegphoto'] = $bin[0];
1175			}
1176			$matches = null;
1177			if(preg_match('/cn=([^,]+),'.preg_quote($this->personalContactsDN,'/').'$/i',$entry['dn'],$matches))
1178			{
1179				// personal addressbook
1180				$contact['owner'] = $GLOBALS['egw']->accounts->name2id($matches[1],'account_lid','u');
1181				$contact['private'] = 0;
1182			}
1183			elseif(preg_match('/cn=([^,]+),'.preg_quote($this->sharedContactsDN,'/').'$/i',$entry['dn'],$matches))
1184			{
1185				// group addressbook
1186				$contact['owner'] = $GLOBALS['egw']->accounts->name2id($matches[1],'account_lid','g');
1187				$contact['private'] = 0;
1188			}
1189			else
1190			{
1191				// accounts
1192				$contact['owner'] = 0;
1193				$contact['private'] = 0;
1194			}
1195			foreach(array(
1196				'createtimestamp' => 'created',
1197				'modifytimestamp' => 'modified',
1198			) as $ldapFieldName => $egwFieldName)
1199			{
1200				if(!empty($entry[$ldapFieldName][0]))
1201				{
1202					$contact[$egwFieldName] = $this->_ldap2ts($entry[$ldapFieldName][0]);
1203				}
1204			}
1205			$contacts[] = $contact;
1206		}
1207		return $contacts;
1208	}
1209
1210	/**
1211	 * Creates a timestamp from the date returned by the ldap server
1212	 *
1213	 * @internal
1214	 * @param string $date YYYYmmddHHiiss
1215	 * @return int
1216	 */
1217	static function _ldap2ts($date)
1218	{
1219		return gmmktime(substr($date,8,2),substr($date,10,2),substr($date,12,2),
1220			substr($date,4,2),substr($date,6,2),substr($date,0,4));
1221	}
1222
1223	/**
1224	 * Create LDAP date-value from timestamp
1225	 *
1226	 * @param integer $ts
1227	 * @return string
1228	 */
1229	static function _ts2ldap($ts)
1230	{
1231		return gmdate('YmdHis', $ts).'.0Z';
1232	}
1233
1234	/**
1235	 * check if $baseDN exists. If not create it
1236	 *
1237	 * @param string $baseDN cn=xxx,ou=yyy,ou=contacts,$this->allContactsDN
1238	 * @return boolean/string false on success or string with error-message
1239	 */
1240	function _check_create_dn($baseDN)
1241	{
1242		// check if $baseDN exists. If not create new one
1243		if(@ldap_read($this->ds, $baseDN, 'objectclass=*'))
1244		{
1245			return false;
1246		}
1247		//error_log(__METHOD__."('$baseDN') !ldap_read({$this->ds}, '$baseDN', 'objectclass=*') ldap_errno()=".ldap_errno($this->ds).', ldap_error()='.ldap_error($this->ds).get_class($this));
1248		if(ldap_errno($this->ds) != 32 || substr($baseDN,0,3) != 'cn=')
1249		{
1250			error_log(__METHOD__."('$baseDN') baseDN does NOT exist and we cant/wont create it! ldap_errno()=".ldap_errno($this->ds).', ldap_error()='.ldap_error($this->ds));
1251			return $this->_error(__LINE__);	// baseDN does NOT exist and we cant/wont create it
1252		}
1253
1254		list(,$ou) = explode(',',$baseDN);
1255		$adminDS = null;
1256		foreach(array(
1257			'ou=contacts,'.$this->allContactsDN,
1258			$ou.',ou=contacts,'.$this->allContactsDN,
1259			$baseDN,
1260		) as $dn)
1261		{
1262			if (!@ldap_read($this->ds, $dn, 'objectclass=*') && ldap_errno($this->ds) == 32)
1263			{
1264				// entry does not exist, lets try to create it
1265				list($top) = explode(',',$dn);
1266				list($var,$val) = explode('=',$top);
1267				$data = array(
1268					'objectClass' => $var == 'cn' ? 'organizationalRole' : 'organizationalUnit',
1269					$var => $val,
1270				);
1271				// create a admin connection to add the needed DN
1272				if (!isset($adminDS)) $adminDS = Api\Ldap::factory();
1273				if(!@ldap_add($adminDS, $dn, $data))
1274				{
1275					//echo "<p>ldap_add($adminDS,'$dn',".print_r($data,true).")</p>\n";
1276					$err = lang("Can't create dn %1",$dn).': '.$this->_error(__LINE__,$adminDS);
1277					return $err;
1278				}
1279			}
1280		}
1281
1282		return false;
1283	}
1284
1285	/**
1286	 * error message for failed ldap operation
1287	 *
1288	 * @param int $line
1289	 * @return string
1290	 */
1291	function _error($line,$ds=null)
1292	{
1293		return ldap_error($ds ? $ds : $this->ds).': '.__CLASS__.': '.$line;
1294	}
1295
1296	/**
1297	 * Special handling for mapping of eGW contact-data to the evolutionPerson objectclass
1298	 *
1299	 * Please note: all regular fields are already copied!
1300	 *
1301	 * @internal
1302	 * @param array &$ldapContact already copied fields according to the mapping
1303	 * @param array $data eGW contact data
1304	 * @param boolean $isUpdate
1305	 */
1306	function _egw2evolutionperson(&$ldapContact,$data,$isUpdate)
1307	{
1308		if(!empty($data['cat_id']))
1309		{
1310			$ldapContact['category'] = array();
1311			foreach(is_array($data['cat_id']) ? $data['cat_id'] : explode(',',$data['cat_id'])  as $cat)
1312			{
1313				$ldapContact['category'][] = Api\Translation::convert(
1314					Api\Categories::id2name($cat),$this->charset,'utf-8');
1315			}
1316		}
1317		foreach(array(
1318			'postaladdress' => $data['adr_one_street'] .'$'. $data['adr_one_locality'] .', '. $data['adr_one_region'] .'$'. $data['adr_one_postalcode'] .'$$'. $data['adr_one_countryname'],
1319			'homepostaladdress' => $data['adr_two_street'] .'$'. $data['adr_two_locality'] .', '. $data['adr_two_region'] .'$'. $data['adr_two_postalcode'] .'$$'. $data['adr_two_countryname'],
1320		) as $attr => $value)
1321		{
1322			if($value != '$, $$$')
1323			{
1324				$ldapContact[$attr] = Api\Translation::convert($value,$this->charset,'utf-8');
1325			}
1326			elseif($isUpdate)
1327			{
1328				$ldapContact[$attr] = array();
1329			}
1330		}
1331		// save the phone number of the primary contact and not the eGW internal field-name
1332		if ($data['tel_prefer'] && $data[$data['tel_prefer']])
1333		{
1334			$ldapContact['primaryphone'] = $data[$data['tel_prefer']];
1335		}
1336		elseif($isUpdate)
1337		{
1338			$ldapContact['primaryphone'] = array();
1339		}
1340	}
1341
1342	/**
1343	 * Special handling for mapping data of the evolutionPerson objectclass to eGW contact
1344	 *
1345	 * Please note: all regular fields are already copied!
1346	 *
1347	 * @internal
1348	 * @param array &$contact already copied fields according to the mapping
1349	 * @param array $data eGW contact data
1350	 */
1351	function _evolutionperson2egw(&$contact,$data)
1352	{
1353		if ($data['category'] && is_array($data['category']))
1354		{
1355			$contact['cat_id'] = array();
1356			foreach($data['category'] as $iii => $cat)
1357			{
1358				if (!is_int($iii)) continue;
1359
1360				$contact['cat_id'][] = $GLOBALS['egw']->categories->name2id($cat);
1361			}
1362			if ($contact['cat_id']) $contact['cat_id'] = implode(',',$contact['cat_id']);
1363		}
1364		if ($data['primaryphone'])
1365		{
1366			unset($contact['tel_prefer']);	// to not find itself
1367			$contact['tel_prefer'] = array_search($data['primaryphone'][0],$contact);
1368		}
1369	}
1370
1371	/**
1372	 * Special handling for mapping data of the inetOrgPerson objectclass to eGW contact
1373	 *
1374	 * Please note: all regular fields are already copied!
1375	 *
1376	 * @internal
1377	 * @param array &$contact already copied fields according to the mapping
1378	 * @param array $data eGW contact data
1379	 */
1380	function _inetorgperson2egw(&$contact, $data, $cn='cn')
1381	{
1382		$matches = null;
1383		if(empty($data['givenname'][0]))
1384		{
1385			$parts = explode($data['sn'][0], $data[$cn][0]);
1386			$contact['n_prefix'] = trim($parts[0]);
1387			$contact['n_suffix'] = trim($parts[1]);
1388		}
1389		// iOS addressbook either use "givenname surname" or "surname givenname" depending on contact preference display-order
1390		// in full name, so we need to check for both when trying to parse prefix, middle name and suffix form full name
1391		elseif (preg_match($preg='/^(.*) *'.preg_quote($data['givenname'][0], '/').' *(.*) *'.preg_quote($data['sn'][0], '/').' *(.*)$/', $data[$cn][0], $matches) ||
1392			preg_match($preg='/^(.*) *'.preg_quote($data['sn'][0], '/').'[, ]*(.*) *'.preg_quote($data['givenname'][0], '/').' *(.*)$/', $data[$cn][0], $matches))
1393		{
1394			list(,$contact['n_prefix'], $contact['n_middle'], $contact['n_suffix']) = $matches;
1395			//error_log(__METHOD__."() preg_match('$preg', '{$data[$cn][0]}') = ".array2string($matches));
1396		}
1397		else
1398		{
1399			$contact['n_prefix'] = $contact['n_suffix'] = $contact['n_middle'] = '';
1400		}
1401		//error_log(__METHOD__."(, data=array($cn=>{$data[$cn][0]}, sn=>{$data['sn'][0]}, givenName=>{$data['givenname'][0]}), cn='$cn') returning with contact=array(n_prefix={$contact['n_prefix']}, n_middle={$contact['n_middle']}, n_suffix={$contact['n_suffix']}) ".function_backtrace());
1402	}
1403
1404	/**
1405	 * Special handling for mapping data of posixAccount objectclass to eGW contact
1406	 *
1407	 * Please note: all regular fields are already copied!
1408	 *
1409	 * @internal
1410	 * @param array &$contact already copied fields according to the mapping
1411	 * @param array $data eGW contact data
1412	 */
1413	function _posixaccount2egw(&$contact,$data)
1414	{
1415		unset($contact);	// not used, but required by function signature
1416		static $shadowExpireNow=null;
1417		if (!isset($shadowExpireNow)) $shadowExpireNow = floor((time()-date('Z'))/86400);
1418
1419		// exclude expired or deactivated accounts
1420		if (isset($data['shadowexpire']) && $data['shadowexpire'][0] <= $shadowExpireNow)
1421		{
1422			return false;
1423		}
1424	}
1425
1426	/**
1427	 * Special handling for mapping data of the mozillaAbPersonAlpha objectclass to eGW contact
1428	 *
1429	 * Please note: all regular fields are already copied!
1430	 *
1431	 * @internal
1432	 * @param array &$contact already copied fields according to the mapping
1433	 * @param array $data eGW contact data
1434	 */
1435	function _mozillaabpersonalpha2egw(&$contact,$data)
1436	{
1437		if ($data['c'])
1438		{
1439			$contact['adr_one_countryname'] = Api\Country::get_full_name($data['c'][0]);
1440		}
1441	}
1442
1443	/**
1444	 * Special handling for mapping of eGW contact-data to the mozillaAbPersonAlpha objectclass
1445	 *
1446	 * Please note: all regular fields are already copied!
1447	 *
1448	 * @internal
1449	 * @param array &$ldapContact already copied fields according to the mapping
1450	 * @param array $data eGW contact data
1451	 * @param boolean $isUpdate
1452	 */
1453	function _egw2mozillaabpersonalpha(&$ldapContact,$data,$isUpdate)
1454	{
1455		if ($data['adr_one_countrycode'])
1456		{
1457			$ldapContact['c'] = $data['adr_one_countrycode'];
1458		}
1459		elseif ($data['adr_one_countryname'])
1460		{
1461			$ldapContact['c'] = Api\Country::country_code($data['adr_one_countryname']);
1462			if ($ldapContact['c'] && strlen($ldapContact['c']) > 2)	// Bad countryname when "custom" selected!
1463			{
1464				$ldapContact['c'] = array(); // should return error...
1465			}
1466		}
1467		elseif ($isUpdate)
1468		{
1469			$ldapContact['c'] = array();
1470		}
1471	}
1472
1473	/**
1474	 * Special handling for mapping data of the mozillaOrgPerson objectclass to eGW contact
1475	 *
1476	 * Please note: all regular fields are already copied!
1477	 *
1478	 * @internal
1479	 * @param array &$contact already copied fields according to the mapping
1480	 * @param array $data eGW contact data
1481	 */
1482	function _mozillaorgperson2egw(&$contact,$data)
1483	{
1484		unset($contact, $data);	// not used, but required by function signature
1485		// no special handling necessary, as it supports two distinct attributes: c, cn
1486	}
1487
1488	/**
1489	 * Special handling for mapping of eGW contact-data to the mozillaOrgPerson objectclass
1490	 *
1491	 * Please note: all regular fields are already copied!
1492	 *
1493	 * @internal
1494	 * @param array &$ldapContact already copied fields according to the mapping
1495	 * @param array $data eGW contact data
1496	 * @param boolean $isUpdate
1497	 */
1498	function _egw2mozillaorgperson(&$ldapContact,$data,$isUpdate)
1499	{
1500		if ($data['adr_one_countrycode'])
1501		{
1502			$ldapContact['c'] = $data['adr_one_countrycode'];
1503			if ($isUpdate) $ldapContact['co'] = array();
1504		}
1505		elseif ($data['adr_one_countryname'])
1506		{
1507			$ldapContact['c'] = Api\Country::country_code($data['adr_one_countryname']);
1508			if ($ldapContact['c'] && strlen($ldapContact['c']) > 2)	// Bad countryname when "custom" selected!
1509			{
1510				$ldapContact['c'] = array(); // should return error...
1511			}
1512		}
1513		elseif ($isUpdate)
1514		{
1515			$ldapContact['c'] = $ldapContact['co'] = array();
1516		}
1517		//error_log(__METHOD__."() adr_one_countrycode='{$data['adr_one_countrycode']}', adr_one_countryname='{$data['adr_one_countryname']}' --> c=".array2string($ldapContact['c']).', co='.array2string($ldapContact['co']));
1518	}
1519
1520	/**
1521	 * Change the ownership of contacts owned by a given account
1522	 *
1523	 * @param int $account_id account-id of the old owner
1524	 * @param int $new_owner account-id of the new owner
1525	 */
1526	function change_owner($account_id,$new_owner)
1527	{
1528		error_log(__METHOD__."($account_id,$new_owner) not yet implemented");
1529	}
1530}
1531