1<?php
2/**
3 * EGroupware API - Contacts storage object
4 *
5 * @link http://www.egroupware.org
6 * @author Cornelius Weiss <egw-AT-von-und-zu-weiss.de>
7 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8 * @package addressbook
9 * @copyright (c) 2005-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
10 * @copyright (c) 2005/6 by Cornelius Weiss <egw@von-und-zu-weiss.de>
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 * Contacts storage object
21 *
22 * The contact storage has 3 operation modi (contact_repository):
23 * - sql: contacts are stored in the SQL table egw_addressbook & egw_addressbook_extra (custom fields)
24 * - ldap: contacts are stored in LDAP (accounts have to be stored in LDAP too!!!).
25 *   Custom fields are not availible in that case!
26 * - sql-ldap: contacts are read and searched in SQL, but saved to both SQL and LDAP.
27 *   Other clients (Thunderbird, ...) can use LDAP readonly. The get maintained via eGroupWare only.
28 *
29 * The accounts can be stored in SQL or LDAP too (account_repository):
30 * If the account-repository is different from the contacts-repository, the filter all (no owner set)
31 * will only search the contacts and NOT the accounts! Only the filter accounts (owner=0) shows accounts.
32 *
33 * If sql-ldap is used as contact-storage (LDAP is managed from eGroupWare) the filter all, searches
34 * the accounts in the SQL contacts-table too. Change in made in LDAP, are not detected in that case!
35 */
36
37class Storage
38{
39	/**
40	 * name of customefields table
41	 *
42	 * @var string
43	 */
44	var $extra_table = 'egw_addressbook_extra';
45
46	/**
47	* @var string
48	*/
49	var $extra_id = 'contact_id';
50
51	/**
52	* @var string
53	*/
54	var $extra_owner = 'contact_owner';
55
56	/**
57	* @var string
58	*/
59	var $extra_key = 'contact_name';
60
61	/**
62	* @var string
63	*/
64	var $extra_value = 'contact_value';
65
66	/**
67	 * view for distributionlistsmembership
68	 *
69	 * @var string
70	 */
71	var $distributionlist_view ='(SELECT contact_id, egw_addressbook_lists.list_id as list_id, egw_addressbook_lists.list_name as list_name, egw_addressbook_lists.list_owner as list_owner FROM egw_addressbook_lists, egw_addressbook2list where egw_addressbook_lists.list_id=egw_addressbook2list.list_id) d_view ';
72	var $distributionlist_tabledef = array();
73	/**
74	* @var string
75	*/
76	var $distri_id = 'contact_id';
77
78	/**
79	* @var string
80	*/
81	var $distri_owner = 'list_owner';
82
83	/**
84	* @var string
85	*/
86	var $distri_key = 'list_id';
87
88	/**
89	* @var string
90	*/
91	var $distri_value = 'list_name';
92
93	/**
94	 * Contact repository in 'sql' or 'ldap'
95	 *
96	 * @var string
97	 */
98	var $contact_repository = 'sql';
99
100	/**
101	 * Grants as  account_id => rights pairs
102	 *
103	 * @var array
104	 */
105	var $grants = array();
106
107	/**
108	 *  userid of current user
109	 *
110	 * @var int
111	 */
112	var $user;
113
114	/**
115	 * memberships of the current user
116	 *
117	 * @var array
118	 */
119	var $memberships;
120
121	/**
122	 * In SQL we can search all columns, though a view make on real sense
123	 */
124	var $sql_cols_not_to_search = array(
125		'jpegphoto','owner','tid','private','cat_id','etag',
126		'modified','modifier','creator','created','tz','account_id',
127		'uid','carddav_name','freebusy_uri','calendar_uri',
128		'geo','pubkey',
129	);
130	/**
131	 * columns to search, if we search for a single pattern
132	 *
133	 * @var array
134	 */
135	var $columns_to_search = array();
136	/**
137	 * extra columns to search if accounts are included, eg. account_lid
138	 *
139	 * @var array
140	 */
141	var $account_extra_search = array();
142	/**
143	 * columns to search for accounts, if stored in different repository
144	 *
145	 * @var array
146	 */
147	var $account_cols_to_search = array();
148
149	/**
150	 * customfields name => array(...) pairs
151	 *
152	 * @var array
153	 */
154	var $customfields = array();
155	/**
156	 * content-types as name => array(...) pairs
157	 *
158	 * @var array
159	 */
160	var $content_types = array();
161
162	/**
163	 * Directory to store striped photo or public keys in VFS directory of entry
164	 */
165	const FILES_DIRECTORY = '.files';
166	const FILES_PHOTO = '.files/photo.jpeg';
167	const FILES_PGP_PUBKEY = '.files/pgp-pubkey.asc';
168	const FILES_SMIME_PUBKEY =  '.files/smime-pubkey.crt';
169
170	/**
171	 * Constant for bit-field "contact_files" storing what files are available
172	 */
173	const FILES_BIT_PHOTO = 1;
174	const FILES_BIT_PGP_PUBKEY = 2;
175	const FILES_BIT_SMIME_PUBKEY = 4;
176
177	/**
178	 * These fields are options for checking for duplicate contacts
179	 *
180	 * @var array
181	 */
182	public static $duplicate_fields = array(
183		'n_given'           => 'first name',
184		'n_middle'          => 'middle name',
185		'n_family'          => 'last name',
186		'contact_bday'      => 'birthday',
187		'org_name'          => 'Organisation',
188		'org_unit'          => 'Department',
189		'adr_one_locality'  => 'Location',
190		'contact_title'     => 'title',
191		'contact_email'     => 'business email',
192		'contact_email_home'=> 'email (private)',
193	);
194
195	/**
196	* Special content type to indicate a deleted addressbook
197	*
198	* @var String;
199	*/
200	const DELETED_TYPE = 'D';
201
202	/**
203	 * total number of matches of last search
204	 *
205	 * @var int
206	 */
207	var $total;
208
209	/**
210	 * storage object: sql (Sql) or ldap (addressbook_ldap) backend class
211	 *
212	 * @var Sql
213	 */
214	var $somain;
215	/**
216	 * storage object for accounts, if not identical to somain (eg. accounts in ldap, contacts in sql)
217	 *
218	 * @var Ldap
219	 */
220	var $so_accounts;
221	/**
222	 * account repository sql or ldap
223	 *
224	 * @var string
225	 */
226	var $account_repository = 'sql';
227	/**
228	 * custom fields backend
229	 *
230	 * @var Sql
231	 */
232	var $soextra;
233	var $sodistrib_list;
234
235	/**
236	 * Constructor
237	 *
238	 * @param string $contact_app ='addressbook' used for acl->get_grants()
239	 * @param Api\Db $db =null
240	 */
241	function __construct($contact_app='addressbook',Api\Db $db=null)
242	{
243		$this->db     = is_null($db) ? $GLOBALS['egw']->db : $db;
244
245		$this->user = $GLOBALS['egw_info']['user']['account_id'];
246		$this->memberships = $GLOBALS['egw']->accounts->memberships($this->user,true);
247
248		// account backend used
249		if ($GLOBALS['egw_info']['server']['account_repository'])
250		{
251			$this->account_repository = $GLOBALS['egw_info']['server']['account_repository'];
252		}
253		elseif ($GLOBALS['egw_info']['server']['auth_type'])
254		{
255			$this->account_repository = $GLOBALS['egw_info']['server']['auth_type'];
256		}
257		$this->customfields = Api\Storage\Customfields::get('addressbook');
258		// contacts backend (contacts in LDAP require accounts in LDAP!)
259		if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap')
260		{
261			$this->contact_repository = 'ldap';
262			$this->somain = new Ldap();
263			$this->columns_to_search = $this->somain->search_attributes;
264		}
265		else	// sql or sql->ldap
266		{
267			if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap')
268			{
269				$this->contact_repository = 'sql-ldap';
270			}
271			$this->somain = new Sql($db);
272
273			// remove some columns, absolutly not necessary to search in sql
274			$this->columns_to_search = array_diff(array_values($this->somain->db_cols),$this->sql_cols_not_to_search);
275		}
276		$this->grants = $this->get_grants($this->user,$contact_app);
277
278		if ($this->account_repository != 'sql' && $this->contact_repository == 'sql')
279		{
280			if ($this->account_repository != $this->contact_repository)
281			{
282				$class = 'EGroupware\\Api\\Contacts\\'.ucfirst($this->account_repository);
283				$this->so_accounts = new $class();
284				$this->account_cols_to_search = $this->so_accounts->search_attributes;
285			}
286			else
287			{
288				$this->account_extra_search = array('uid');
289			}
290		}
291		if ($this->contact_repository == 'sql' || $this->contact_repository == 'sql-ldap')
292		{
293			$tda2list = $this->db->get_table_definitions('api','egw_addressbook2list');
294			$tdlists = $this->db->get_table_definitions('api','egw_addressbook_lists');
295			$this->distributionlist_tabledef = array('fd' => array(
296					$this->distri_id => $tda2list['fd'][$this->distri_id],
297					$this->distri_owner => $tdlists['fd'][$this->distri_owner],
298        	    	$this->distri_key => $tdlists['fd'][$this->distri_key],
299					$this->distri_value => $tdlists['fd'][$this->distri_value],
300				), 'pk' => array(), 'fk' => array(), 'ix' => array(), 'uc' => array(),
301			);
302		}
303		// ToDo: it should be the other way arround, the backend should set the grants it uses
304		$this->somain->grants =& $this->grants;
305
306		if($this->somain instanceof Sql)
307		{
308			$this->soextra =& $this->somain;
309		}
310		else
311		{
312			$this->soextra = new Sql($db);
313		}
314
315		$this->content_types = Api\Config::get_content_types('addressbook');
316		if (!$this->content_types)
317		{
318			$this->content_types = array('n' => array(
319				'name' => 'contact',
320				'options' => array(
321					'template' => 'addressbook.edit',
322					'icon' => 'navbar.png'
323			)));
324		}
325
326		// Add in deleted type, if holding deleted contacts
327		$config = Api\Config::read('phpgwapi');
328		if($config['history'])
329		{
330			$this->content_types[self::DELETED_TYPE] = array(
331				'name'	=>	lang('Deleted'),
332				'options' =>	array(
333					'template'	=>	'addressbook.edit',
334					'icon'		=>	'deleted.png'
335				)
336			);
337		}
338	}
339
340	/**
341	 * Get grants for a given user, taking into account static LDAP ACL
342	 *
343	 * @param int $user
344	 * @param string $contact_app ='addressbook'
345	 * @return array
346	 */
347	function get_grants($user, $contact_app='addressbook', $preferences=null)
348	{
349		if (!isset($preferences)) $preferences = $GLOBALS['egw_info']['user']['preferences'];
350
351		if ($user)
352		{
353			// contacts backend (contacts in LDAP require accounts in LDAP!)
354			if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap')
355			{
356				// static grants from ldap: all rights for the own personal addressbook and the group ones of the meberships
357				$grants = array($user => ~0);
358				foreach($GLOBALS['egw']->accounts->memberships($user,true) as $gid)
359				{
360					$grants[$gid] = ~0;
361				}
362			}
363			else	// sql or sql->ldap
364			{
365				// group grants are now grants for the group addressbook and NOT grants for all its members,
366				// therefor the param false!
367				$grants = $GLOBALS['egw']->acl->get_grants($contact_app,false,$user);
368			}
369			// add grants for accounts: if account_selection not in ('none','groupmembers'): everyone has read access,
370			// if he has not set the hide_accounts preference
371			// ToDo: be more specific for 'groupmembers', they should be able to see the groupmembers
372			if (!in_array($preferences['common']['account_selection'], array('none','groupmembers')))
373			{
374				$grants[0] = Api\Acl::READ;
375			}
376			// add account grants for admins (only for current user!)
377			if ($user == $this->user && $this->is_admin())	// admin rights can be limited by ACL!
378			{
379				$grants[0] = Api\Acl::READ;	// admins always have read-access
380				if (!$GLOBALS['egw']->acl->check('account_access',16,'admin')) $grants[0] |= Api\Acl::EDIT;
381				if (!$GLOBALS['egw']->acl->check('account_access',4,'admin'))  $grants[0] |= Api\Acl::ADD;
382				if (!$GLOBALS['egw']->acl->check('account_access',32,'admin')) $grants[0] |= Api\Acl::DELETE;
383			}
384			// allow certain groups to edit contact-data of accounts
385			if (self::allow_account_edit($user))
386			{
387				$grants[0] |= Api\Acl::READ|Api\Acl::EDIT;
388			}
389		}
390		// no user, eg. setup or not logged in, allow read access to accounts
391		else
392		{
393			$grants = [0 => Api\Acl::READ];
394		}
395		//error_log(__METHOD__."($user, '$contact_app') returning ".array2string($grants));
396		return $grants;
397	}
398
399	/**
400	 * Check if the user is an admin (can unconditionally edit accounts)
401	 *
402	 * We check now the admin ACL for edit users, as the admin app does it for editing accounts.
403	 *
404	 * @param array $contact =null for future use, where admins might not be admins for all accounts
405	 * @return boolean
406	 */
407	function is_admin($contact=null)
408	{
409		unset($contact);	// not (yet) used
410
411		return isset($GLOBALS['egw_info']['user']['apps']['admin']) && !$GLOBALS['egw']->acl->check('account_access',16,'admin');
412	}
413
414	/**
415	 * Check if current user is in a group, which is allowed to edit accounts
416	 *
417	 * @param int $user =null default $this->user
418	 * @return boolean
419	 */
420	function allow_account_edit($user=null)
421	{
422		return $GLOBALS['egw_info']['server']['allow_account_edit'] &&
423			array_intersect($GLOBALS['egw_info']['server']['allow_account_edit'],
424				$GLOBALS['egw']->accounts->memberships($user ? $user : $this->user, true));
425	}
426
427	/**
428	 * Read all customfields of the given id's
429	 *
430	 * @param int|array $ids
431	 * @param array $field_names =null custom fields to read, default all
432	 * @return array id => name => value
433	 */
434	function read_customfields($ids,$field_names=null)
435	{
436		return $this->soextra->read_customfields($ids,$field_names);
437	}
438
439	/**
440	 * Read all distributionlists of the given id's
441	 *
442	 * @param int|array $ids
443	 * @return array id => name => value
444	 */
445	function read_distributionlist($ids, $dl_allowed=array())
446	{
447		if ($this->contact_repository == 'ldap')
448		{
449			return array();	// ldap does not support distributionlists
450		}
451		foreach($ids as $key => $id)
452		{
453			if (!is_numeric($id)) unset($ids[$key]);
454		}
455		if (!$ids) return array();	// nothing to do, eg. all these contacts are in ldap
456		$fields = array();
457		$filter[$this->distri_id]=$ids;
458		if (count($dl_allowed)) $filter[$this->distri_key]=$dl_allowed;
459		$distri_view = str_replace(') d_view',' and '.$this->distri_id.' in ('.implode(',',$ids).')) d_view',$this->distributionlist_view);
460		#_debug_array($this->distributionlist_tabledef);
461		foreach($this->db->select($distri_view, '*', $filter, __LINE__, __FILE__,
462			false, 'ORDER BY '.$this->distri_id, false, 0, '', $this->distributionlist_tabledef) as $row)
463		{
464			if ((isset($row[$this->distri_id])&&strlen($row[$this->distri_value])>0))
465			{
466				$fields[$row[$this->distri_id]][$row[$this->distri_key]] = $row[$this->distri_value].' ('.
467					Api\Accounts::username($row[$this->distri_owner]).')';
468			}
469		}
470		return $fields;
471	}
472
473	/**
474	 * changes the data from the db-format to your work-format
475	 *
476	 * it gets called everytime when data is read from the db
477	 * This function needs to be reimplemented in the derived class
478	 *
479	 * @param array $data
480	 */
481	function db2data($data)
482	{
483		return $data;
484	}
485
486	/**
487	 * changes the data from your work-format to the db-format
488	 *
489	 * It gets called everytime when data gets writen into db or on keys for db-searches
490	 * this needs to be reimplemented in the derived class
491	 *
492	 * @param array $data
493	 */
494	function data2db($data)
495	{
496		return $data;
497	}
498
499	/**
500	* deletes contact entry including custom fields
501	*
502	* @param mixed $contact array with id or just the id
503	* @param int $check_etag =null
504	* @return boolean|int true on success or false on failiure, 0 if etag does not match
505	*/
506	function delete($contact,$check_etag=null)
507	{
508		if (is_array($contact)) $contact = $contact['id'];
509
510		$where = array('id' => $contact);
511		if ($check_etag) $where['etag'] = $check_etag;
512
513		// delete mainfields
514		if ($this->somain->delete($where))
515		{
516			// delete customfields, can return 0 if there are no customfields
517			if(!($this->somain instanceof Sql))
518			{
519				$this->soextra->delete_customfields(array($this->extra_id => $contact));
520			}
521
522			// delete from distribution list(s)
523			$this->remove_from_list($contact);
524
525			if ($this->contact_repository == 'sql-ldap')
526			{
527				if ($contact['account_id'])
528				{
529					// LDAP uses the uid attributes for the contact-id (dn),
530					// which need to be the account_lid for accounts!
531					$contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']);
532				}
533				(new Ldap())->delete($contact);
534			}
535			return true;
536		}
537		return $check_etag ? 0 : false;		// if etag given, we return 0 on failure, thought it could also mean the whole contact does not exist
538	}
539
540	/**
541	* saves contact data including custom fields
542	*
543	* @param array &$contact contact data from etemplate::exec
544	* @return bool false on success, errornumber on failure
545	*/
546	function save(&$contact)
547	{
548		// save mainfields
549		if ($contact['id'] && $this->contact_repository != $this->account_repository && is_object($this->so_accounts) &&
550			($this->contact_repository == 'sql' && !is_numeric($contact['id']) ||
551			 $this->contact_repository == 'ldap' && is_numeric($contact['id'])))
552		{
553			$this->so_accounts->data = $this->data2db($contact);
554			$error_nr = $this->so_accounts->save();
555			$contact['id'] = $this->so_accounts->data['id'];
556		}
557		else
558		{
559			// contact_repository sql-ldap (accounts in ldap) the person_id is the uid (account_lid)
560			// for the sql write here we need to find out the existing contact_id
561			if ($this->contact_repository == 'sql-ldap' && $contact['id'] && !is_numeric($contact['id']) &&
562				$contact['account_id'] && ($old = $this->somain->read(array('account_id' => $contact['account_id']))))
563			{
564				$contact['id'] = $old['id'];
565			}
566			$this->somain->data = $this->data2db($contact);
567
568			if (!($error_nr = $this->somain->save()))
569			{
570				$contact['id'] = $this->somain->data['id'];
571				$contact['uid'] = $this->somain->data['uid'];
572				$contact['etag'] = $this->somain->data['etag'];
573				$contact['files'] = $this->somain->data['files'];
574
575				if ($this->contact_repository == 'sql-ldap')
576				{
577					$data = $this->somain->data;
578					if ($contact['account_id'])
579					{
580						// LDAP uses the uid attributes for the contact-id (dn),
581						// which need to be the account_lid for accounts!
582						$data['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']);
583					}
584					$error_nr = (new Ldap())->save($data);
585				}
586			}
587		}
588		if($error_nr) return $error_nr;
589
590		return false;	// no error
591	}
592
593	/**
594	 * reads contact data including custom fields
595	 *
596	 * @param int|string $contact_id contact_id or 'a'.account_id
597	 * @return array|boolean data if row could be retrived else False
598	*/
599	function read($contact_id)
600	{
601		if (empty($contact_id))
602		{
603			return false;	// no need to pass to backend, will fail anyway
604		}
605		if (!is_array($contact_id) && substr($contact_id,0,8) == 'account:')
606		{
607			$contact_id = array('account_id' => (int) substr($contact_id,8));
608		}
609		// read main data
610		$backend = $this->get_backend($contact_id);
611		if (!($contact = $backend->read($contact_id)))
612		{
613			return $contact;
614		}
615		$dl_list=$this->read_distributionlist(array($contact['id']));
616		if (count($dl_list)) $contact['distrib_lists']=implode("\n",$dl_list[$contact['id']]);
617		return $this->db2data($contact);
618	}
619
620	/**
621	 * @param array $ids
622	 * @param ?boolean $deleted false: no deleted, true: only deleted, null: both
623	 * @return array contact_id => array of array with sharing info
624	 */
625	function read_shared(array $ids, $deleted=false)
626	{
627		$contacts = [];
628		if (method_exists($backend = $this->get_backend($ids[0]), 'read_shared'))
629		{
630			foreach($backend->read_shared($ids, $deleted) as $shared)
631			{
632				$contacts[$shared['contact_id']][] = $shared;
633			}
634		}
635		return $contacts;
636	}
637
638	/**
639	 * searches db for rows matching searchcriteria
640	 *
641	 * '*' and '?' are replaced with sql-wildcards '%' and '_'
642	 *
643	 * @param array|string $criteria array of key and data cols, OR string to search over all standard search fields
644	 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return
645	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
646	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
647	 * @param string $wildcard ='' appended befor and after each criteria
648	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
649	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
650	 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num)
651	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
652	 *  $filter['cols_to_search'] limit search columns to given columns, otherwise $this->columns_to_search is used
653	 * @param string $join ='' sql to do a join (only used by sql backend!), eg. " RIGHT JOIN egw_accounts USING(account_id)"
654	 * @param boolean $ignore_acl =false true: no acl check
655	 * @return array of matching rows (the row is an array of the cols) or False
656	 */
657	function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='', $ignore_acl=false)
658	{
659		//error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')");
660
661		// Handle 'None' country option
662		if(is_array($filter) && $filter['adr_one_countrycode'] == '-custom-')
663		{
664			$filter[] = 'adr_one_countrycode IS NULL';
665			unset($filter['adr_one_countrycode']);
666		}
667		// Hide deleted items unless type is specifically deleted
668		if(!is_array($filter)) $filter = $filter ? (array) $filter : array();
669
670		if (isset($filter['cols_to_search']))
671		{
672			$cols_to_search = $filter['cols_to_search'];
673			unset($filter['cols_to_search']);
674		}
675
676		// if no tid set or tid==='' do NOT return deleted entries ($tid === null returns all entries incl. deleted)
677		if(!array_key_exists('tid', $filter) || $filter['tid'] === '')
678		{
679			if ($join && strpos($join,'RIGHT JOIN') !== false)	// used eg. to search for groups
680			{
681				$filter[] = '(contact_tid != \'' . self::DELETED_TYPE . '\' OR contact_tid IS NULL)';
682			}
683			else
684			{
685				$filter[] = 'contact_tid != \'' . self::DELETED_TYPE . '\'';
686			}
687		}
688		elseif(is_null($filter['tid']))
689		{
690			unset($filter['tid']);	// return all entries incl. deleted
691		}
692		$backend = $this->get_backend(null, isset($filter['list']) && $filter['list'] < 0 ? 0 : $filter['owner']);
693		// single string to search for --> create so_sql conformant search criterial for the standard search columns
694		if ($criteria && !is_array($criteria))
695		{
696			$op = 'OR';
697			$wildcard = '%';
698			$search = $criteria;
699			$criteria = array();
700
701			if (isset($cols_to_search))
702			{
703				$cols = $cols_to_search;
704			}
705			elseif ($backend === $this->somain)
706			{
707				$cols = $this->columns_to_search;
708			}
709			else
710			{
711				$cols = $this->account_cols_to_search;
712			}
713			if($backend instanceof Sql)
714			{
715				// Keep a string, let the parent handle it
716				$criteria = $search;
717
718				foreach($cols as $key => &$col)
719				{
720					if($col != Sql::EXTRA_VALUE &&
721						$col != Sql::EXTRA_TABLE.'.'.Sql::EXTRA_VALUE &&
722						!array_key_exists($col, $backend->db_cols))
723					{
724						if(!($col = array_search($col, $backend->db_cols)))
725						{
726							// Can't search this column, it will error if we try
727							unset($cols[$key]);
728						}
729					}
730					if ($col=='contact_id') $col='egw_addressbook.contact_id';
731				}
732
733				$backend->columns_to_search = $cols;
734			}
735			else
736			{
737				foreach($cols as $col)
738				{
739					// remove from LDAP backend not understood use-AND-syntax
740					$criteria[$col] = str_replace(' +',' ',$search);
741				}
742			}
743		}
744		if (is_array($criteria) && count($criteria))
745		{
746			$criteria = $this->data2db($criteria);
747		}
748		if (is_array($filter) && count($filter))
749		{
750			$filter = $this->data2db($filter);
751		}
752		else
753		{
754			$filter = $filter ? array($filter) : array();
755		}
756		// get the used backend for the search and call it's search method
757		$rows = $backend->search($criteria, $only_keys, $order_by, $extra_cols,
758			$wildcard, $empty, $op, $start, $filter, $join, false, $ignore_acl);
759
760		$this->total = $backend->total;
761
762		if ($rows)
763		{
764			foreach($rows as $n => $row)
765			{
766				$rows[$n] = $this->db2data($row);
767			}
768
769			// allow other apps to hook into search
770			Api\Hooks::process(array(
771				'hook_location' => 'contacts_search',
772				'criteria'      => $criteria,
773				'filter'        => $filter,
774				'ignore_acl'    => $ignore_acl,
775				'obj'           => $this,
776				'rows'          => &$rows,
777				'total'         => &$this->total,
778			), array(), true);	// true = no permission check
779		}
780		return $rows;
781	}
782
783	/**
784	 * Query organisations by given parameters
785	 *
786	 * @var array $param
787	 * @var string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group
788	 * @var int $param[owner] addressbook to search
789	 * @var string $param[search] search pattern for org_name
790	 * @var string $param[searchletter] letter the org_name need to start with
791	 * @var int $param[start]
792	 * @var int $param[num_rows]
793	 * @var string $param[sort] ASC or DESC
794	 * @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit
795	 */
796	function organisations($param)
797	{
798		if (!method_exists($this->somain,'organisations'))
799		{
800			$this->total = 0;
801			return false;
802		}
803		if ($param['search'] && !is_array($param['search']))
804		{
805			$search = $param['search'];
806			$param['search'] = array();
807			if($this->somain instanceof Sql)
808			{
809				// Keep the string, let the parent deal with it
810				$param['search'] = $search;
811			}
812			else
813			{
814				foreach($this->columns_to_search as $col)
815				{
816					if ($col != 'contact_value') $param['search'][$col] = $search;	// we dont search the customfields
817				}
818			}
819		}
820		if (is_array($param['search']) && count($param['search']))
821		{
822			$param['search'] = $this->data2db($param['search']);
823		}
824		if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '')
825		{
826			$param['col_filter'][] = 'contact_tid != \'' . self::DELETED_TYPE . '\'';
827		}
828		elseif(is_null($param['col_filter']['tid']))
829		{
830			unset($param['col_filter']['tid']);	// return all entries incl. deleted
831		}
832
833		$rows = $this->somain->organisations($param);
834		$this->total = $this->somain->total;
835
836		if (!$rows) return array();
837
838		foreach($rows as $n => $row)
839		{
840			if (strpos($row['org_name'],'&')!==false) $row['org_name'] = str_replace('&','*AND*',$row['org_name']);
841			$rows[$n]['id'] = 'org_name:'.$row['org_name'];
842			foreach(array(
843				'org_unit' => lang('departments'),
844				'adr_one_locality' => lang('locations'),
845			) as $by => $by_label)
846			{
847				if ($row[$by.'_count'] > 1)
848				{
849					$rows[$n][$by] = $row[$by.'_count'].' '.$by_label;
850				}
851				else
852				{
853					if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]);
854					$rows[$n]['id'] .= '|||'.$by.':'.$row[$by];
855				}
856			}
857		}
858		return $rows;
859	}
860
861	/**
862	 * Find contacts that appear to be duplicates
863	 *
864	 * @param Array $param
865	 * @param string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group
866	 * @param int $param[owner] addressbook to search
867	 * @param string $param[search] search pattern for org_name
868	 * @param string $param[searchletter] letter the org_name need to start with
869	 * @param int $param[start]
870	 * @param int $param[num_rows]
871	 * @param string $param[sort] ASC or DESC
872	 *
873	 * @return array of arrays
874	 */
875	public function duplicates($param)
876	{
877		if (!method_exists($this->somain,'duplicates'))
878		{
879			$this->total = 0;
880			return false;
881		}
882		if ($param['search'] && !is_array($param['search']))
883		{
884			$search = $param['search'];
885			$param['search'] = array();
886			if($this->somain instanceof Sql)
887			{
888				// Keep the string, let the parent deal with it
889				$param['search'] = $search;
890			}
891			else
892			{
893				foreach($this->columns_to_search as $col)
894				{
895					// we don't search the customfields
896					if ($col != 'contact_value') $param['search'][$col] = $search;
897				}
898			}
899		}
900		if (is_array($param['search']) && count($param['search']))
901		{
902			$param['search'] = $this->data2db($param['search']);
903		}
904		if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '')
905		{
906			$param['col_filter'][] = $this->somain->table_name.'.contact_tid != \'' . self::DELETED_TYPE . '\'';
907		}
908		elseif(is_null($param['col_filter']['tid']))
909		{
910			// return all entries including deleted
911			unset($param['col_filter']['tid']);
912		}
913		if(array_key_exists('filter', $param) && $param['filter'] != '')
914		{
915			$param['owner'] = $param['filter'];
916			unset($param['filter']);
917		}
918		if(array_key_exists('owner', $param['col_filter']) && $param['col_filter']['owner'] != '')
919		{
920			$param['owner'] = $param['col_filter']['owner'];
921			unset($param['col_filter']['owner']);
922		}
923
924
925		$rows = $this->somain->duplicates($param);
926		$this->total = $this->somain->total;
927
928		if (!$rows) return array();
929
930		foreach($rows as $n => $row)
931		{
932			$rows[$n]['id'] = 'duplicate:';
933			foreach(array_keys(static::$duplicate_fields) as $by)
934			{
935				if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]);
936				if($row[$by])
937				{
938					$rows[$n]['id'] .= '|||'.$by.':'.$row[$by];
939				}
940			}
941		}
942		return $rows;
943	}
944
945 	/**
946	 * gets all contact fields from database
947	 *
948	 * @return array of (internal) field-names
949	 */
950	function get_contact_columns()
951	{
952		$fields = $this->get_fields('all');
953		foreach (array_keys((array)$this->customfields) as $cfield)
954		{
955			$fields[] = '#'.$cfield;
956		}
957		return $fields;
958	}
959
960	/**
961	 * delete / move all contacts of an addressbook
962	 *
963	 * @param array $data
964	 * @param int $data['account_id'] owner to change
965	 * @param int $data['new_owner']  new owner or 0 for delete
966	 */
967	function deleteaccount($data)
968	{
969		$account_id = $data['account_id'];
970		$new_owner =  $data['new_owner'];
971
972		if (!$new_owner)
973		{
974			$this->somain->delete(array('owner' => $account_id));	// so_sql_cf::delete() takes care of cfs too
975
976			if (method_exists($this->somain, 'get_lists') &&
977				($lists = $this->somain->get_lists($account_id)))
978			{
979				$this->somain->delete_list(array_keys($lists));
980			}
981		}
982		else
983		{
984			$this->somain->change_owner($account_id,$new_owner);
985		}
986	}
987
988	/**
989	 * return the backend, to be used for the given $contact_id
990	 *
991	 * @param array|string|int $keys =null
992	 * @param int $owner =null account_id of owner or 0 for accounts
993	 * @return Sql|Ldap|Ads|Univention
994	 */
995	function get_backend($keys=null,$owner=null)
996	{
997		if ($owner === '') $owner = null;
998
999		$contact_id = !is_array($keys) ? $keys :
1000			(isset($keys['id']) ? $keys['id'] : $keys['contact_id']);
1001
1002		if ($this->contact_repository != $this->account_repository && is_object($this->so_accounts) &&
1003			(!is_null($owner) && !$owner || is_array($keys) && $keys['account_id'] || !is_null($contact_id) &&
1004			($this->contact_repository == 'sql' && (!is_numeric($contact_id) && !is_array($contact_id) )||
1005			 $this->contact_repository == 'ldap' && is_numeric($contact_id))))
1006		{
1007			return $this->so_accounts;
1008		}
1009		return $this->somain;
1010	}
1011
1012	/**
1013	 * Returns the supported, all or unsupported fields of the backend (depends on owner or contact_id)
1014	 *
1015	 * @param sting $type ='all' 'supported', 'unsupported' or 'all'
1016	 * @param mixed $contact_id =null
1017	 * @param int $owner =null account_id of owner or 0 for accounts
1018	 * @return array with eGW contact field names
1019	 */
1020	function get_fields($type='all',$contact_id=null,$owner=null)
1021	{
1022		$def = $this->db->get_table_definitions('api','egw_addressbook');
1023
1024		$all_fields = array();
1025		foreach(array_keys($def['fd']) as $field)
1026		{
1027			$all_fields[] = substr($field,0,8) == 'contact_' ? substr($field,8) : $field;
1028		}
1029		if ($type == 'all')
1030		{
1031			return $all_fields;
1032		}
1033		$backend = $this->get_backend($contact_id,$owner);
1034
1035		$supported_fields = method_exists($backend, 'supported_fields') ? $backend->supported_fields() : $all_fields;
1036
1037		if ($type == 'supported')
1038		{
1039			return $supported_fields;
1040		}
1041		return array_diff($all_fields,$supported_fields);
1042	}
1043
1044	/**
1045	 * Migrates an SQL contact storage to LDAP, SQL-LDAP or back to SQL
1046	 *
1047	 * @param string|array $type comma-separated list or array of:
1048	 *  - "contacts" contacts to ldap
1049	 *  - "accounts" accounts to ldap
1050	 *  - "accounts-back" accounts back to sql (for sql-ldap!)
1051	 *  - "sql" contacts and accounts to sql
1052	 *  - "accounts-back-ads" accounts back from ads to sql
1053	 */
1054	function migrate2ldap($type)
1055	{
1056		//error_log(__METHOD__."(".array2string($type).")");
1057		$sql_contacts  = new Sql();
1058		if ($type == 'accounts-back-ads')
1059		{
1060			$ldap_contacts = new Ads();
1061		}
1062		else
1063		{
1064			// we need an admin connection
1065			$ds = $GLOBALS['egw']->ldap->ldapConnect();
1066			$ldap_contacts = new Ldap(null, $ds);
1067		}
1068
1069		if (!is_array($type)) $type = explode(',', $type);
1070
1071		$start = $n = 0;
1072		$num = 100;
1073
1074		// direction SQL --> LDAP, either only accounts, or only contacts or both
1075		if (($do = array_intersect($type, array('contacts', 'accounts'))))
1076		{
1077			$filter = count($do) == 2 ? null :
1078				array($do[0] == 'contacts' ? 'contact_owner != 0' : 'contact_owner = 0');
1079
1080			while (($contacts = $sql_contacts->search(false,false,'n_family,n_given','','',false,'AND',
1081				array($start,$num),$filter)))
1082			{
1083				foreach($contacts as $contact)
1084				{
1085					if ($contact['account_id']) $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']);
1086
1087					$ldap_contacts->data = $contact;
1088					$n++;
1089					if (!($err = $ldap_contacts->save()))
1090					{
1091						echo '<p style="margin: 0px;">'.$n.': '.$contact['n_fn'].
1092							($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> LDAP</p>\n";
1093					}
1094					else
1095					{
1096						echo '<p style="margin: 0px; color: red;">'.$n.': '.$contact['n_fn'].
1097							($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."</p>\n";
1098					}
1099				}
1100				$start += $num;
1101			}
1102		}
1103		// direction LDAP --> SQL: either "sql" (contacts and accounts) or "accounts-back" (only accounts)
1104		if (($do = array_intersect(array('accounts-back','sql'), $type)))
1105		{
1106			//error_log(__METHOD__."(".array2string($type).") do=".array2string($type));
1107			$filter = in_array('sql', $do) ? null : array('owner' => 0);
1108
1109			foreach($ldap_contacts->search(false,false,'n_family,n_given','','',false,'AND',
1110				false, $filter) as $contact)
1111			{
1112				//error_log(__METHOD__."(".array2string($type).") do=".array2string($type)." migrating ".array2string($contact));
1113				if ($contact['jpegphoto'])	// photo is NOT read by LDAP backend on search, need to do an extra read
1114				{
1115					$contact = $ldap_contacts->read($contact['id']);
1116				}
1117				$old_contact_id = $contact['id'];
1118				unset($contact['id']);	// ldap uid/account_lid
1119				if ($contact['account_id'] && ($old = $sql_contacts->read(array('account_id' => $contact['account_id']))))
1120				{
1121					$contact['id'] = $old['id'];
1122				}
1123				$sql_contacts->data = $contact;
1124
1125				$n++;
1126				if (!($err = $sql_contacts->save()))
1127				{
1128					echo '<p style="margin: 0px;">'.$n.': '.$contact['n_fn'].
1129						($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> SQL (".
1130						($contact['owner']?lang('User'):lang('Contact')).")<br>\n";
1131
1132					$new_contact_id = $sql_contacts->data['id'];
1133					echo "&nbsp;&nbsp;&nbsp;&nbsp;" . $old_contact_id . " --> " . $new_contact_id . " / ";
1134
1135					$this->db->update('egw_links',array(
1136						'link_id1' => $new_contact_id,
1137					),array(
1138						'link_app1' => 'addressbook',
1139						'link_id1' => $old_contact_id
1140					),__LINE__,__FILE__);
1141
1142					$this->db->update('egw_links',array(
1143						'link_id2' => $new_contact_id,
1144					),array(
1145						'link_app2' => 'addressbook',
1146						'link_id2' => $old_contact_id
1147					),__LINE__,__FILE__);
1148					echo "</p>\n";
1149				}
1150				else
1151				{
1152					echo '<p style="margin: 0px; color: red;">'.$n.': '.$contact['n_fn'].
1153						($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."</p>\n";
1154				}
1155			}
1156		}
1157	}
1158
1159	/**
1160	 * Get the availible distribution lists for a user
1161	 *
1162	 * @param int $required =Api\Acl::READ required rights on the list or multiple rights or'ed together,
1163	 * 	to return only lists fullfilling all the given rights
1164	 * @param string $extra_labels =null first labels if given (already translated)
1165	 * @return array with id => label pairs or false if backend does not support lists
1166	 */
1167	function get_lists($required=Api\Acl::READ,$extra_labels=null)
1168	{
1169		$lists = is_array($extra_labels) ? $extra_labels : array();
1170
1171		if (method_exists($this->somain,'get_lists'))
1172		{
1173			$uids = array();
1174			foreach($this->grants as $uid => $rights)
1175			{
1176				// only requests groups / list in accounts addressbook for read
1177				if (!$uid && $required != Api\Acl::READ) continue;
1178
1179				if (($rights & $required) == $required)
1180				{
1181					$uids[] = $uid;
1182				}
1183			}
1184
1185			foreach($this->somain->get_lists($uids) as $list_id => $data)
1186			{
1187				$lists[$list_id] = $data['list_name'];
1188				if ($data['list_owner'] != $this->user)
1189				{
1190					$lists[$list_id] .= ' ('.Api\Accounts::username($data['list_owner']).')';
1191				}
1192			}
1193		}
1194
1195		// add groups for all backends, if accounts addressbook is not hidden &
1196		// preference has not turned them off
1197		if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== '1' &&
1198				$GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_groups_as_lists'] == '0')
1199		{
1200			foreach($GLOBALS['egw']->accounts->search(array(
1201				'type' => 'groups'
1202			)) as $account_id => $group)
1203			{
1204				$lists[(string)$account_id] = Api\Accounts::format_username($group['account_lid'], '', '', $account_id);
1205			}
1206		}
1207
1208		return $lists;
1209	}
1210
1211	/**
1212	 * Get the availible distribution lists for givens users and groups
1213	 *
1214	 * @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid)
1215	 * @param string $member_attr ='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute
1216	 * @param boolean $limit_in_ab =false if true only return members from the same owners addressbook
1217	 * @return array with list_id => array(list_id,list_name,list_owner,...) pairs
1218	 */
1219	function read_lists($keys,$member_attr=null,$limit_in_ab=false)
1220	{
1221		$backend = (string)$limit_in_ab === '0' && $this->so_accounts ? $this->so_accounts : $this->somain;
1222		if (!method_exists($backend, 'get_lists')) return false;
1223
1224		return $backend->get_lists($keys,null,$member_attr,$limit_in_ab);
1225	}
1226
1227	/**
1228	 * Adds / updates a distribution list
1229	 *
1230	 * @param string|array $keys list-name or array with column-name => value pairs to specify the list
1231	 * @param int $owner user- or group-id
1232	 * @param array $contacts =array() contacts to add (only for not yet existing lists!)
1233	 * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name'
1234	 * @return int|boolean integer list_id or false on error
1235	 */
1236	function add_list($keys,$owner,$contacts=array(),array &$data=array())
1237	{
1238		$backend = (string)$owner === '0' && $this->so_accounts ? $this->so_accounts : $this->somain;
1239		if (!method_exists($backend, 'add_list')) return false;
1240
1241		return $backend->add_list($keys,$owner,$contacts,$data);
1242	}
1243
1244	/**
1245	 * Adds contact(s) to a distribution list
1246	 *
1247	 * @param int|array $contact contact_id(s)
1248	 * @param int $list list-id
1249	 * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array()
1250	 * @return false on error
1251	 */
1252	function add2list($contact,$list,array $existing=null)
1253	{
1254		if (!method_exists($this->somain,'add2list')) return false;
1255
1256		return $this->somain->add2list($contact,$list,$existing);
1257	}
1258
1259	/**
1260	 * Removes one contact from distribution list(s)
1261	 *
1262	 * @param int|array $contact contact_id(s)
1263	 * @param int $list =null list-id or null to remove from all lists
1264	 * @return false on error
1265	 */
1266	function remove_from_list($contact,$list=null)
1267	{
1268		if (!method_exists($this->somain,'remove_from_list')) return false;
1269
1270		return $this->somain->remove_from_list($contact,$list);
1271	}
1272
1273	/**
1274	 * Deletes a distribution list (incl. it's members)
1275	 *
1276	 * @param int|array $list list_id(s)
1277	 * @return number of members deleted or false if list does not exist
1278	 */
1279	function delete_list($list)
1280	{
1281		if (!method_exists($this->somain,'delete_list')) return false;
1282
1283		return $this->somain->delete_list($list);
1284	}
1285
1286	/**
1287	 * Read data of a distribution list
1288	 *
1289	 * @param int $list list_id
1290	 * @return array of data or false if list does not exist
1291	 */
1292	function read_list($list)
1293	{
1294		if (!method_exists($this->somain,'read_list')) return false;
1295
1296		return $this->somain->read_list($list);
1297	}
1298
1299	/**
1300	 * Check if distribution lists are availible for a given addressbook
1301	 *
1302	 * @param int|string $owner ='' addressbook (eg. 0 = accounts), default '' = "all" addressbook (uses the main backend)
1303	 * @return boolean
1304	 */
1305	function lists_available($owner='')
1306	{
1307		$backend =& $this->get_backend(null,$owner);
1308
1309		return method_exists($backend,'read_list');
1310	}
1311
1312	/**
1313	 * Get ctag (max list_modified as timestamp) for lists
1314	 *
1315	 * @param int|array $owner =null null for all lists user has access too
1316	 * @return int
1317	 */
1318	function lists_ctag($owner=null)
1319	{
1320		if (!method_exists($this->somain,'lists_ctag')) return 0;
1321
1322		return $this->somain->lists_ctag($owner);
1323	}
1324}
1325