Sql::search()   F
last analyzed

Complexity

Conditions 60
Paths > 20000

Size

Total Lines 173
Code Lines 88

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 60
eloc 88
nc 815280
nop 12
dl 0
loc 173
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * EGroupware API: Contacts - SQL storage
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @package api
8
 * @subpackage contacts
9
 * @copyright (c) 2006-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Contacts;
15
16
use EGroupware\Api;
17
18
/**
19
 * Contacts - SQL storage
20
 */
21
class Sql extends Api\Storage
22
{
23
	/**
24
	 * name of custom fields table
25
	 *
26
	 * @var string
27
	 */
28
	var $account_repository = 'sql';
29
	var $contact_repository = 'sql';
30
	var $grants;
31
32
	/**
33
	 * join to show only active account (and not already expired ones)
34
	 */
35
	const ACCOUNT_ACTIVE_JOIN = ' LEFT JOIN egw_accounts ON egw_addressbook.account_id=egw_accounts.account_id ';
36
	/**
37
	 * filter to show only active account (and not already expired or deactived ones)
38
	 * UNIX_TIMESTAMP(NOW()) gets replaced with value of time() in the code!
39
	 */
40
	const ACOUNT_ACTIVE_FILTER = "(account_expires IS NULL OR account_expires = -1 OR account_expires > UNIX_TIMESTAMP(NOW())) AND (account_type IS NULL OR account_type!='u' OR account_status='A')";
41
42
	/**
43
	 * internal name of the id, gets mapped to uid
44
	 *
45
	 * @var string
46
	 */
47
	var $contacts_id='id';
48
49
	/**
50
	 * Name of the table for distribution lists
51
	 *
52
	 * @var string
53
	 */
54
	var $lists_table = 'egw_addressbook_lists';
55
	/**
56
	 * Name of the table with the members (contacts) of the distribution lists
57
	 *
58
	 * @var string
59
	 */
60
	var $ab2list_table = 'egw_addressbook2list';
61
62
	const EXTRA_TABLE = 'egw_addressbook_extra';
63
	const EXTRA_VALUE = 'contact_value';
64
65
	/**
66
	 * Constructor
67
	 *
68
	 * @param Api\Db $db =null
69
	 */
70
	function __construct(Api\Db $db=null)
71
	{
72
		parent::__construct('api', 'egw_addressbook', self::EXTRA_TABLE,
73
			'contact_', '_name', '_value', '_id', $db);
74
75
		// Get custom fields from addressbook instead of api
76
		$this->customfields = Api\Storage\Customfields::get('addressbook');
77
78
		if ($GLOBALS['egw_info']['server']['account_repository'])
79
		{
80
			$this->account_repository = $GLOBALS['egw_info']['server']['account_repository'];
81
		}
82
		elseif ($GLOBALS['egw_info']['server']['auth_type'])
83
		{
84
			$this->account_repository = $GLOBALS['egw_info']['server']['auth_type'];
85
		}
86
		if ($GLOBALS['egw_info']['server']['contact_repository'])
87
		{
88
			$this->contact_repository = $GLOBALS['egw_info']['server']['contact_repository'];
89
		}
90
	}
91
92
	/**
93
	 * Query organisations by given parameters
94
	 *
95
	 * @var array $param
96
	 * @var string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group
97
	 * @var int $param[owner] addressbook to search
98
	 * @var string $param[search] search pattern for org_name
99
	 * @var string $param[searchletter] letter the org_name need to start with
100
	 * @var array $param[col_filter] filter
101
	 * @var string $param[search] or'ed search pattern
102
	 * @var array $param[advanced_search] indicator that advanced search is active
103
	 * @var string $param[op] (operator like AND or OR; will be passed when advanced search is active)
104
	 * @var string $param[wildcard] (wildcard like % or empty or not set (for no wildcard); will be passed when advanced search is active)
105
	 * @var int $param[start]
106
	 * @var int $param[num_rows]
107
	 * @var string $param[sort] ASC or DESC
108
	 * @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit
109
	 */
110
	function organisations($param)
111
	{
112
		$filter = is_array($param['col_filter']) ? $param['col_filter'] : array();
113
		$join = '';
114
		$op = 'OR';
115
		if (isset($param['op']) && !empty($param['op'])) $op = $param['op'];
116
		$advanced_search = false;
117
		if (isset($param['advanced_search']) && !empty($param['advanced_search'])) $advanced_search = true;
118
		$wildcard ='%';
119
		if ($advanced_search || (isset($param['wildcard']) && !empty($param['wildcard']))) $wildcard = ($param['wildcard']?$param['wildcard']:'');
120
121
		// fix cat_id filter to search in comma-separated multiple cats and return subcats
122
		if ($filter['cat_id'])
123
		{
124
			$filter[] = $this->_cat_filter($filter['cat_id']);
125
			unset($filter['cat_id']);
126
		}
127
		// add filter for read ACL in sql, if user is NOT the owner of the addressbook
128
		if ($param['owner'] && $param['owner'] == $GLOBALS['egw_info']['user']['account_id'])
129
		{
130
			$filter['owner'] = $param['owner'];
131
		}
132
		else
133
		{
134
			// we have no private grants in addressbook at the moment, they have then to be added here too
135
			if ($param['owner'])
136
			{
137
				if (!$this->grants[(int) $filter['owner']]) return false;	// we have no access to that addressbook
138
139
				$filter['owner'] = $param['owner'];
140
				$filter['private'] = 0;
141
			}
142
			else	// search all addressbooks, incl. accounts
143
			{
144
				if ($this->account_repository != 'sql' && $this->contact_repository != 'sql-ldap')
145
				{
146
					$filter[] = $this->table_name.'.contact_owner != 0';	// in case there have been accounts in sql previously
147
				}
148
				$filter[] = "(".$this->table_name.".contact_owner=".(int)$GLOBALS['egw_info']['user']['account_id'].
149
					(!$this->grants ? ')' :
150
					" OR contact_private=0 AND ".$this->table_name.".contact_owner IN (".
151
					implode(',',array_keys($this->grants))."))");
152
			}
153
			if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== 'none')
154
			{
155
				$join .= self::ACCOUNT_ACTIVE_JOIN;
156
				if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '0')
157
				{
158
					$filter[] = str_replace('UNIX_TIMESTAMP(NOW())',time(),self::ACOUNT_ACTIVE_FILTER);
159
				}
160
				else
161
				{
162
					$filter[] = 'egw_accounts.account_id IS NULL';
163
				}
164
			}
165
		}
166
		if ($param['searchletter'])
167
		{
168
			$filter[] = 'org_name '.$this->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($param['searchletter'].'%');
169
		}
170
		else
171
		{
172
			$filter[] = "org_name != ''";// AND org_name IS NOT NULL";
173
		}
174
		if (isset($filter['list']))
175
		{
176
			if ($filter['list'] < 0)
177
			{
178
				$join .= " JOIN egw_acl ON $this->table_name.account_id=acl_account AND acl_appname='phpgw_group' AND ".
179
					$this->db->expression('egw_acl', array('acl_location' => $filter['list']));
180
			}
181
			else
182
			{
183
				$join .= " JOIN $this->ab2list_table ON $this->table_name.contact_id=$this->ab2list_table.contact_id AND ".
184
					$this->db->expression($this->ab2list_table, array('list_id' => $filter['list']));
185
			}
186
			unset($filter['list']);
187
		}
188
		$sort = $param['sort'] == 'DESC' ? 'DESC' : 'ASC';
189
190
		list(,$by) = explode(',',$param['org_view']);
191
		if (!$by)
192
		{
193
			$extra = array(
194
				'COUNT(DISTINCT egw_addressbook.contact_id) AS org_count',
195
				"COUNT(DISTINCT CASE WHEN org_unit IS NULL THEN '' ELSE org_unit END) AS org_unit_count",
196
				"COUNT(DISTINCT CASE WHEN adr_one_locality IS NULL THEN '' ELSE adr_one_locality END) AS adr_one_locality_count",
197
			);
198
			$append = "GROUP BY org_name ORDER BY org_name $sort";
199
		}
200
		else	// by adr_one_location or org_unit
201
		{
202
			// org total for more then one $by
203
			$by_expr = $by == 'org_unit_count' ? "COUNT(DISTINCT CASE WHEN org_unit IS NULL THEN '' ELSE org_unit END)" :
204
				"COUNT(DISTINCT CASE WHEN adr_one_locality IS NULL THEN '' ELSE adr_one_locality END)";
205
			parent::search($param['search'],array('org_name'),
206
				"GROUP BY org_name HAVING $by_expr > 1 ORDER BY org_name $sort", array(
207
				"NULL AS $by",
208
				'1 AS is_main',
209
				'COUNT(DISTINCT egw_addressbook.contact_id) AS org_count',
210
				"COUNT(DISTINCT CASE WHEN org_unit IS NULL THEN '' ELSE org_unit END) AS org_unit_count",
211
				"COUNT(DISTINCT CASE WHEN adr_one_locality IS NULL THEN '' ELSE adr_one_locality END) AS adr_one_locality_count",
212
			),$wildcard,false,$op/*'OR'*/,'UNION',$filter,$join);
213
			// org by location
214
			parent::search($param['search'],array('org_name'),
215
				"GROUP BY org_name,$by ORDER BY org_name $sort,$by $sort", array(
216
				"CASE WHEN $by IS NULL THEN '' ELSE $by END AS $by",
217
				'0 AS is_main',
218
				'COUNT(DISTINCT egw_addressbook.contact_id) AS org_count',
219
				"COUNT(DISTINCT CASE WHEN org_unit IS NULL THEN '' ELSE org_unit END) AS org_unit_count",
220
				"COUNT(DISTINCT CASE WHEN adr_one_locality IS NULL THEN '' ELSE adr_one_locality END) AS adr_one_locality_count",
221
			),$wildcard,false,$op/*'OR'*/,'UNION',$filter,$join);
222
			$append = "ORDER BY org_name $sort,is_main DESC,$by $sort";
223
		}
224
		$rows = parent::search($param['search'],array('org_name'),$append,$extra,$wildcard,false,$op/*'OR'*/,
225
			array($param['start'],$param['num_rows']),$filter,$join);
226
227
		if (!$rows) return false;
228
229
		// query the values for *_count == 1, to display them instead
230
		$filter['org_name'] = $orgs = array();
231
		foreach($rows as $row)
232
		{
233
			if ($row['org_unit_count'] == 1 || $row['adr_one_locality_count'] == 1)
234
			{
235
				$filter['org_name'][$row['org_name']] = $row['org_name'];	// use as key too to have every org only once
236
			}
237
			$org_key = $row['org_name'].($by ? '|||'.($row[$by] || $row[$by.'_count']==1 ? $row[$by] : '|||') : '');
238
			$row['group_count'] = $row['org_count'];
239
			$orgs[$org_key] = $row;
240
		}
241
		unset($rows);
242
243
		if (count($filter['org_name']))
244
		{
245
			foreach((array) parent::search(null, array('org_name','org_unit','adr_one_locality'),
246
				'GROUP BY org_name,org_unit,adr_one_locality',
247
				'',$wildcard,false,$op/*'AND'*/,false,$filter,$join) as $row)
248
			{
249
				$org_key = $row['org_name'].($by ? '|||'.$row[$by] : '');
250
				if ($orgs[$org_key]['org_unit_count'] == 1)
251
				{
252
					$orgs[$org_key]['org_unit'] = $row['org_unit'];
253
				}
254
				if ($orgs[$org_key]['adr_one_locality_count'] == 1)
255
				{
256
					$orgs[$org_key]['adr_one_locality'] = $row['adr_one_locality'];
257
				}
258
				if ($by && isset($orgs[$org_key = $row['org_name'].'||||||']))
259
				{
260
					if ($orgs[$org_key]['org_unit_count'] == 1)
261
					{
262
						$orgs[$org_key]['org_unit'] = $row['org_unit'];
263
					}
264
					if ($orgs[$org_key]['adr_one_locality_count'] == 1)
265
					{
266
						$orgs[$org_key]['adr_one_locality'] = $row['adr_one_locality'];
267
					}
268
				}
269
			}
270
		}
271
		return array_values($orgs);
272
	}
273
274
275
	/**
276
	 * Query for duplicate contacts according to given parameters
277
	 *
278
	 * We join egw_addressbook to itself, and count how many fields match.  If
279
	 * enough of the fields we care about match, we count those two records as
280
	 * duplicates.
281
	 *
282
	 * @var array $param
283
	 * @var string $param[grouped_view] 'duplicate', 'duplicate,adr_one_location', 'duplicate,org_name' how to group
284
	 * @var int $param[owner] addressbook to search
285
	 * @var string $param[search] search pattern for org_name
286
	 * @var string $param[searchletter] letter the name need to start with
287
	 * @var array $param[col_filter] filter
288
	 * @var string $param[search] or'ed search pattern
289
	 * @var array $param[advanced_search] indicator that advanced search is active
290
	 * @var string $param[op] (operator like AND or OR; will be passed when advanced search is active)
291
	 * @var string $param[wildcard] (wildcard like % or empty or not set (for no wildcard); will be passed when advanced search is active)
292
	 * @var int $param[start]
293
	 * @var int $param[num_rows]
294
	 * @var string $param[sort] ASC or DESC
295
	 * @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit
296
	 */
297
	function duplicates($param)
298
	{
299
		$join = 'JOIN ' . $this->table_name . ' AS a2 ON ';
300
		$filter = $param['col_filter'];
301
		$op = 'OR';
302
		if (isset($param['op']) && !empty($param['op'])) $op = $param['op'];
303
		$advanced_search = false;
304
		if (isset($param['advanced_search']) && !empty($param['advanced_search'])) $advanced_search = true;
305
		$wildcard ='%';
306
		if ($advanced_search || (isset($param['wildcard']) && !empty($param['wildcard']))) $wildcard = ($param['wildcard']?$param['wildcard']:'');
307
308
		// fix cat_id filter to search in comma-separated multiple cats and return subcats
309
		if ($param['cat_id'])
310
		{
311
			$cat_filter = $this->_cat_filter($filter['cat_id']);
312
			$filter[] = str_replace('cat_id', $this->table_name . '.cat_id', $cat_filter);
313
			$join .= str_replace('cat_id', 'a2.cat_id', $cat_filter) . ' AND ';
314
			unset($filter['cat_id']);
315
		}
316
		if ($filter['tid'])
317
		{
318
			$filter[$this->table_name . '.contact_tid'] = $param['col_filter']['tid'];
319
			$join .= 'a2.contact_tid = ' . $this->db->quote($filter['tid']) . ' AND ';
320
			unset($filter['tid']);
321
		}
322
		else
323
		{
324
			$join .= 'a2.contact_tid != \'D\' AND ';
325
		}
326
		// add filter for read ACL in sql, if user is NOT the owner of the addressbook
327
		if (array_key_exists('owner',$param) && $param['owner'] == $GLOBALS['egw_info']['user']['account_id'])
328
		{
329
			$filter[$this->table_name.'.contact_owner'] = $param['owner'];
330
			$join .= 'a2.contact_owner = ' . $this->db->quote($param['owner']) . ' AND ';
331
		}
332
		else
333
		{
334
			// we have no private grants in addressbook at the moment, they have then to be added here too
335
			if (array_key_exists('owner', $param))
336
			{
337
				if (!$this->grants[(int) $param['owner']]) return false;	// we have no access to that addressbook
338
339
				$filter[$this->table_name.'.contact_owner'] = $param['owner'];
340
				$filter[$this->table_name.'.private'] = 0;
341
				$join .= 'a2.contact_owner = ' . $this->db->quote($param['owner']) . ' AND ';
342
				$join .= 'a2.contact_private = ' . $this->db->quote($filter['private']) . ' AND ';
343
			}
344
			else	// search all addressbooks, incl. accounts
345
			{
346
				if ($this->account_repository != 'sql' && $this->contact_repository != 'sql-ldap')
347
				{
348
					$filter[] = $this->table_name.'.contact_owner != 0';	// in case there have been accounts in sql previously
349
				}
350
				$filter[] = $access = "(".$this->table_name.".contact_owner=".(int)$GLOBALS['egw_info']['user']['account_id'].
351
					" OR {$this->table_name}.contact_private=0 AND ".$this->table_name.".contact_owner IN (".
352
					implode(',',array_keys($this->grants))."))";
353
				$join .= str_replace($this->table_name, 'a2', $access) . ' AND ';
354
			}
355
		}
356
		if ($param['searchletter'])
357
		{
358
			$filter[] = $this->table_name.'.n_fn '.$this->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($param['searchletter'].'%');
359
		}
360
		$sort = $param['sort'] == 'DESC' ? 'DESC' : 'ASC';
361
		$group = $GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields'] ?
362
				explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']):
363
				array('n_family', 'org_name', 'contact_email');
364
		$match_count = $GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_threshold'] ?
365
				$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_threshold'] : 3;
366
367
		$columns = Array();
368
		$extra = Array();
369
		$order = in_array($param['order'], $group) ? $param['order'] : $group[0];
370
		$join .= $this->table_name .'.contact_id != a2.contact_id AND (';
371
		$join_fields = Array();
372
		foreach($group as $field)
373
		{
374
			$extra[] = "IF({$this->table_name}.$field = a2.$field, 1, 0)";
375
			$join_fields[] = $this->table_name . ".$field = a2.$field";
376
			$columns[] = "IF({$this->table_name}.$field = a2.$field, {$this->table_name}.$field, '') AS $field";
377
		}
378
		$extra = Array(
379
			implode('+', $extra) . ' AS match_count'
380
		);
381
		$join .= $this->db->column_data_implode(' OR ',$join_fields) . ')';
382
		if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== 'none')
383
		{
384
			if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '0')
385
			{
386
				$join .=' LEFT JOIN egw_accounts AS account_1 ON egw_addressbook.account_id=account_1.account_id ';
387
				$join .=' LEFT JOIN egw_accounts AS account_2 ON egw_addressbook.account_id=account_2.account_id ';
388
				$filter[] = str_replace(array('UNIX_TIMESTAMP(NOW())', 'account_'),array(time(),'account_1.account_'),self::ACOUNT_ACTIVE_FILTER);
389
				$filter[] = str_replace(array('UNIX_TIMESTAMP(NOW())', 'account_'),array(time(),'account_2.account_'),self::ACOUNT_ACTIVE_FILTER);
390
			}
391
			else
392
			{
393
				$filter[] = 'egw_addressbook.account_id IS NULL and a2.account_id IS NULL';
394
			}
395
		}
396
		$append = " HAVING match_count >= $match_count ORDER BY {$order} $sort, $this->table_name.contact_id";
397
		$columns[] = $this->table_name.'.contact_id AS contact_id';
398
399
		$criteria = array();
400
		if ($param['search'] && !is_array($param['search']))
401
		{
402
			$search_cols = array();
403
			foreach($group as $col)
404
			{
405
				$search_cols[] = $this->table_name . '.' . $col;
406
			}
407
			$search = $this->search2criteria($param['search'],$wildcard,$op, null, $search_cols);
408
			$criteria = array($search);
409
		}
410
		$query = $this->parse_search(array_merge($criteria, $filter), $wildcard, false, ' AND ');
411
412
		$sub_query = $this->db->select($this->table_name,
413
			'DISTINCT ' . implode(', ',array_merge($columns, $extra)),
414
			$query,
415
			False, False, 0, $append, False, -1,
416
			$join
417
		);
418
419
		$columns = implode(', ', $group);
420
		if ($this->db->Type == 'mysql' && $this->db->ServerInfo['version'] >= 4.0)
421
		{
422
			$mysql_calc_rows = 'SQL_CALC_FOUND_ROWS ';
423
		}
424
425
		$rows = $this->db->query(
426
				"SELECT $mysql_calc_rows " . $columns. ', COUNT(contact_id) AS group_count' .
427
				' FROM (' . $sub_query . ') AS matches GROUP BY ' . implode(',',$group) .
428
				' HAVING group_count > 1 ORDER BY ' . $order,
429
				__LINE__, __FILE__, (int)$param['start'],$mysql_calc_rows ? (int)$param['num_rows'] : -1
430
		);
431
432
		// Go through rows and only return one for each pair/triplet/etc. of matches
433
		$dupes = array();
434
		foreach($rows as $key => $row)
435
		{
436
			$row['email'] = $row['contact_email'];
437
			$row['email_home'] = $row['contact_email_home'];
438
			$dupes[] = $this->db2data($row);
439
		}
440
441
		if ($mysql_calc_rows)
442
		{
443
			$this->total = $this->db->query('SELECT FOUND_ROWS()')->fetchColumn();
444
		}
445
		else
446
		{
447
			$this->total = $rows->NumRows();
448
		}
449
		return $dupes;
450
	}
451
452
	/**
453
	 * searches db for rows matching searchcriteria
454
	 *
455
	 * '*' and '?' are replaced with sql-wildcards '%' and '_'
456
	 *
457
	 * For a union-query you call search for each query with $start=='UNION' and one more with only $order_by and $start set to run the union-query.
458
	 *
459
	 * @param array|string $criteria array of key and data cols, OR a SQL query (content for WHERE), fully quoted (!)
460
	 * @param boolean|string|array $only_keys =true True returns only keys, False returns all cols. or
461
	 *	comma seperated list or array of columns to return
462
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
463
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
464
	 * @param string $wildcard ='' appended befor and after each criteria
465
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
466
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
467
	 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num), or 'UNION' for a part of a union query
468
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
469
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
470
	 *	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
471
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
472
	 * @param boolean $ignore_acl =false true: no acl check
473
	 * @return boolean/array of matching rows (the row is an array of the cols) or False
0 ignored issues
show
Documentation Bug introduced by
The doc comment boolean/array at position 0 could not be parsed: Unknown type name 'boolean/array' at position 0 in boolean/array.
Loading history...
474
	 */
475
	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, $ignore_acl=false)
476
	{
477
		if ((int) $this->debug >= 4) echo '<p>'.__METHOD__.'('.array2string($criteria).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',$start,".array2string($filter).",'$join')</p>\n";
478
		//error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by', ".array2string($extra_cols).",'$wildcard','$empty','$op',$start,".array2string($filter).",'$join')");
479
480
		$owner = isset($filter['owner']) ? $filter['owner'] : (isset($criteria['owner']) ? $criteria['owner'] : null);
481
482
		// fix cat_id criteria to search in comma-separated multiple cats and return subcats
483
		if (is_array($criteria) && ($cats = $criteria['cat_id']))
0 ignored issues
show
Unused Code introduced by
The assignment to $cats is dead and can be removed.
Loading history...
484
		{
485
			$criteria = array_merge($criteria, $this->_cat_search($criteria['cat_id']));
486
			unset($criteria['cat_id']);
487
		}
488
		// fix cat_id filter to search in comma-separated multiple cats and return subcats
489
		if (($cats = $filter['cat_id']))
490
		{
491
			if ($filter['cat_id'][0] == '!')
492
			{
493
				$filter['cat_id'] = substr($filter['cat_id'],1);
494
				$not = 'NOT';
495
			}
496
			$filter[] = $this->_cat_filter($filter['cat_id'],$not);
497
			unset($filter['cat_id']);
498
		}
499
500
		// add filter for read ACL in sql, if user is NOT the owner of the addressbook
501
		if (isset($this->grants) && !$ignore_acl &&
502
			!(isset($filter['owner']) && $filter['owner'] == $GLOBALS['egw_info']['user']['account_id']))
503
		{
504
			// add read ACL for groupmembers (they have no
505
			if ($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'groupmembers' &&
506
				(!isset($filter['owner']) || in_array('0',(array)$filter['owner'])))
507
			{
508
				$groupmembers = array();
509
				foreach($GLOBALS['egw']->accounts->memberships($GLOBALS['egw_info']['user']['account_id'],true) as $group_id)
510
				{
511
					if (($members = $GLOBALS['egw']->accounts->members($group_id,true)))
512
					{
513
						$groupmembers = array_merge($groupmembers,$members);
514
					}
515
				}
516
				$groupmember_sql = $this->db->expression($this->table_name, ' OR '.$this->table_name.'.',array(
517
					'account_id' => array_unique($groupmembers),
518
				));
519
			}
520
			// we have no private grants in addressbook at the moment, they have then to be added here too
521
			if (isset($filter['owner']))
522
			{
523
				// no grants for selected owner/addressbook
524
				if (!array_intersect((array)$filter['owner'],array_keys($this->grants)))
525
				{
526
					if (!isset($groupmember_sql)) return false;
527
					$filter[] = substr($groupmember_sql,4);
528
					unset($filter['owner']);
529
				}
530
				// for an owner filter, which does NOT include current user, filter out private entries
531
				elseif (!in_array($GLOBALS['egw_info']['user']['account_id'], (array)$filter['owner']))
532
				{
533
					$filter['private'] = 0;
534
				}
535
				// if multiple addressbooks (incl. current owner) are searched, we need full acl filter
536
				elseif(is_array($filter['owner']) && count($filter['owner']) > 1)
537
				{
538
					$filter[] = "($this->table_name.contact_owner=".(int)$GLOBALS['egw_info']['user']['account_id'].
539
						" OR contact_private=0 AND $this->table_name.contact_owner IN (".
540
						implode(',',array_keys($this->grants)).") $groupmember_sql OR $this->table_name.contact_owner IS NULL)";
541
				}
542
			}
543
			else	// search all addressbooks, incl. accounts
544
			{
545
				if ($this->account_repository != 'sql' && $this->contact_repository != 'sql-ldap')
546
				{
547
					$filter[] = $this->table_name.'.contact_owner != 0';	// in case there have been accounts in sql previously
548
				}
549
				$filter[] = "($this->table_name.contact_owner=".(int)$GLOBALS['egw_info']['user']['account_id'].
550
					($this->grants ? " OR contact_private=0 AND $this->table_name.contact_owner IN (".
551
						implode(',',array_keys($this->grants)).")" : '').
552
					$groupmember_sql." OR $this->table_name.contact_owner IS NULL)";
553
			}
554
		}
555
		if (isset($filter['list']))
556
		{
557
			if ($filter['list'] < 0)
558
			{
559
				$join .= " JOIN egw_acl ON $this->table_name.account_id=acl_account AND acl_appname='phpgw_group' AND ".
560
					$this->db->expression('egw_acl', array('acl_location' => $filter['list']));
561
			}
562
			else
563
			{
564
				$join .= " JOIN $this->ab2list_table ON $this->table_name.contact_id=$this->ab2list_table.contact_id AND ".
565
					$this->db->expression($this->ab2list_table, array('list_id' => $filter['list']));
566
			}
567
			unset($filter['list']);
568
		}
569
		// add join to show only active accounts (only if accounts are shown and in sql and we not already join the accounts table, eg. used by admin)
570
		if ((is_array($owner) ? in_array(0, $owner) : !$owner) && substr($this->account_repository,0,3) == 'sql' &&
571
			strpos($join,$GLOBALS['egw']->accounts->backend->table) === false && !array_key_exists('account_id',$filter))
572
		{
573
			$join .= self::ACCOUNT_ACTIVE_JOIN;
574
			if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '0')
575
			{
576
				$filter[] = str_replace('UNIX_TIMESTAMP(NOW())',time(),self::ACOUNT_ACTIVE_FILTER);
577
			}
578
		}
579
		if ($join || ($criteria && is_string($criteria)) || ($criteria && is_array($criteria) && $order_by))	// search also adds a join for custom fields!
580
		{
581
			switch(gettype($only_keys))
582
			{
583
				case 'boolean':
584
					// Correctly handled by parent class
585
					break;
586
				case 'string':
587
					$only_keys = explode(',',$only_keys);
588
					// fall through
589
			}
590
			// postgres requires that expressions in order by appear in the columns of a distinct select
591
			$all_matches = null;
592
			if ($this->db->Type != 'mysql' && preg_match_all("/(#?[a-zA-Z_.]+) *(<> *''|IS NULL|IS NOT NULL)? *(ASC|DESC)?(,|$)/ui",
593
				$order_by, $all_matches, PREG_SET_ORDER))
594
			{
595
				if (!is_array($extra_cols))	$extra_cols = $extra_cols ? explode(',',$extra_cols) : array();
596
				foreach($all_matches as $matches)
597
				{
598
					$table = '';
599
					$column = $matches[1];
600
					if ($column[0] == '#') continue;	// order by custom field is handeled in so_sql_cf anyway
601
					if (($key = array_search($column, $this->db_cols)) !== false) $column = $key;
602
					if (strpos($column,'.') === false)
603
					{
604
						$table = $column == $this->extra_value ? $this->extra_table : $this->table_name;
605
						if (isset($this->db_cols[$column]))
606
						{
607
							$table .= '.';
608
						}
609
						else
610
						{
611
							$table = '';
612
						}
613
					}
614
					$extra_cols[] = $table.$column.' '.$matches[2];
615
					//_debug_array($matches);
616
					if (!empty($order_by) && $table) // postgres requires explizit order by
617
					{
618
						$order_by = str_replace($matches[0],$table.$column.' '.$matches[2].' '.$matches[3].$matches[4],$order_by);
619
					}
620
				}
621
				//_debug_array($order_by); _debug_array($extra_cols);
622
			}
623
624
			// Understand search by date with wildcard (????.10.??) according to user date preference
625
			if(is_string($criteria) && strpos($criteria, '?') !== false)
626
			{
627
				// First, check for a 'date', with wildcards, in the user's format
628
				$date_regex = str_replace('Q','d',
629
					str_replace(array('Y','m','d','.','-'),
630
						array('(?P<Y>(?:\?|\Q){4})','(?P<m>(?:\?|\Q){2})','(?P<d>(?:\?|\Q){2})','\.','\-'),
631
							$GLOBALS['egw_info']['user']['preferences']['common']['dateformat']));
632
633
				if(preg_match_all('$'.$date_regex.'$', $criteria, $matches))
634
				{
635
					foreach($matches[0] as $m_id => $match)
636
					{
637
						// Birthday is Y-m-d
638
						$criteria = str_replace($match, "*{$matches['Y'][$m_id]}-{$matches['m'][$m_id]}-{$matches['d'][$m_id]}*",$criteria);
639
					}
640
				}
641
			}
642
		}
643
		$rows =& parent::search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter,$join,$need_full_no_count);
644
645
		if ($start === false) $this->total = is_array($rows) ? count($rows) : 0;	// so_sql sets total only for $start !== false!
646
647
		return $rows;
648
	}
649
650
	/**
651
	 * fix cat_id filter to search in comma-separated multiple cats and return subcats
652
	 *
653
	 * @internal
654
	 * @param int|array $cat_id
655
	 * @return string sql to filter by given cat
656
	 */
657
	function _cat_filter($cat_id, $not='')
658
	{
659
		if (!is_object($GLOBALS['egw']->categories))
660
		{
661
			$GLOBALS['egw']->categories = new Api\Categories;
662
		}
663
		foreach($GLOBALS['egw']->categories->return_all_children($cat_id) as $cat)
664
		{
665
			$cat_filter[] = $this->db->concat("','", 'cat_id', "','")." $not LIKE '%,$cat,%'";
666
		}
667
		$cfilter = '('.implode(' OR ',$cat_filter).')';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cat_filter seems to be defined by a foreach iteration on line 663. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
668
		if(!empty($not))
669
		{
670
			$cfilter = "( $cfilter OR cat_id IS NULL )";
671
		}
672
		return $cfilter;
673
	}
674
675
	/**
676
	 * fix cat_id criteria to search in comma-separated multiple cats
677
	 *
678
	 * @internal
679
	 * @param int|array|string $cats
680
	 * @return array of sql-strings to be OR'ed or AND'ed together
681
	 */
682
	function _cat_search($cats)
683
	{
684
		$cat_filter = array();
685
		foreach(is_array($cats) ? $cats : (is_numeric($cats) ? array($cats) : explode(',',$cats)) as $cat)
686
		{
687
			if (is_numeric($cat)) $cat_filter[] = $this->db->concat("','",cat_id,"','")." LIKE '%,$cat,%'";
0 ignored issues
show
Bug introduced by
The constant EGroupware\Api\Contacts\cat_id was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
688
		}
689
		return $cat_filter;
690
	}
691
692
	/**
693
	 * Change the ownership of contacts and distribution-lists owned by a given account
694
	 *
695
	 * @param int $account_id account-id of the old owner
696
	 * @param int $new_owner account-id of the new owner
697
	 */
698
	function change_owner($account_id,$new_owner)
699
	{
700
		if (!$new_owner)	// otherwise we would create an account (contact_owner==0)
701
		{
702
			throw Api\Exception\WrongParameter(__METHOD__."($account_id, $new_owner) new owner must not be 0!");
0 ignored issues
show
Bug introduced by
The function WrongParameter was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

702
			throw /** @scrutinizer ignore-call */ Api\Exception\WrongParameter(__METHOD__."($account_id, $new_owner) new owner must not be 0!");
Loading history...
703
		}
704
		// contacts
705
		$this->db->update($this->table_name,array(
706
			'contact_owner' => $new_owner,
707
		),array(
708
			'contact_owner' => $account_id,
709
		),__LINE__,__FILE__);
710
711
		// cfs
712
		$this->db->update(self::EXTRA_TABLE, array(
713
			'contact_owner' => $new_owner
714
		),array(
715
			'contact_owner' => $account_id
716
		), __LINE__, __FILE__);
717
718
		// lists
719
		$this->db->update($this->lists_table, array(
720
			'list_owner' => $new_owner,
721
		),array(
722
			'list_owner' => $account_id,
723
		),__LINE__,__FILE__);
724
	}
725
726
	/**
727
	 * Get the availible distribution lists for givens users and groups
728
	 *
729
	 * @param array $uids array of user or group id's for $uid_column='list_owners', or values for $uid_column,
730
	 * 	or whole where array: column-name => value(s) pairs
731
	 * @param string $uid_column ='list_owner' column-name or null to use $uids as where array
732
	 * @param string $member_attr =null null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute
733
	 * @param boolean|int|array $limit_in_ab =false if true only return members from the same owners addressbook,
734
	 * 	if int|array only return members from the given owners addressbook(s)
735
	 * @return array with list_id => array(list_id,list_name,list_owner,...) pairs
736
	 */
737
	function get_lists($uids,$uid_column='list_owner',$member_attr=null,$limit_in_ab=false)
738
	{
739
		if (is_array($uids) && array_key_exists('list_id', $uids))
740
		{
741
			$uids[] = $this->db->expression($this->lists_table, $this->lists_table.'.',array('list_id' => $uids['list_id']));
742
			unset($uids['list_id']);
743
		}
744
		$lists = array();
745
		foreach($this->db->select($this->lists_table,'*',$uid_column?array($uid_column=>$uids):$uids,__LINE__,__FILE__,
746
			false,'ORDER BY list_owner<>'.(int)$GLOBALS['egw_info']['user']['account_id'].',list_name') as $row)
747
		{
748
			if ($member_attr) $row['members'] = array();
749
			$lists[$row['list_id']] = $row;
750
		}
751
		if ($lists && $member_attr && in_array($member_attr,array('contact_id','contact_uid','caldav_name')))
752
		{
753
			if ($limit_in_ab)
754
			{
755
				$in_ab_join = " JOIN $this->lists_table ON $this->lists_table.list_id=$this->ab2list_table.list_id AND $this->lists_table.";
756
				if (!is_bool($limit_in_ab))
757
				{
758
					$in_ab_join .= $this->db->expression($this->lists_table, array('list_owner'=>$limit_in_ab));
759
				}
760
				else
761
				{
762
					$in_ab_join .= "list_owner=$this->table_name.contact_owner";
763
				}
764
			}
765
			foreach($this->db->select($this->ab2list_table,"$this->ab2list_table.list_id,$this->table_name.$member_attr",
766
				$this->db->expression($this->ab2list_table, $this->ab2list_table.'.', array('list_id'=>array_keys($lists))),
767
				__LINE__,__FILE__,false,$member_attr=='contact_id' ? '' :
768
				'',false,0,"JOIN $this->table_name ON $this->ab2list_table.contact_id=$this->table_name.contact_id".$in_ab_join) as $row)
769
			{
770
				$lists[$row['list_id']]['members'][] = $row[$member_attr];
771
			}
772
		}
773
		/* groups as list are implemented currently in Contacts\Storage::get_lists() for all backends
774
		if ($uid_column == 'list_owner' && in_array(0, (array)$uids) && (!$limit_in_ab || in_array(0, (array)$limit_in_ab)))
775
		{
776
			foreach($GLOBALS['egw']->accounts->search(array(
777
				'type' => 'groups'
778
			)) as $account_id => $group)
779
			{
780
				$list = array(
781
					'list_id' => $account_id,
782
					'list_name' => Api\Accounts::format_username($group['account_lid'], '', '', $account_id),
783
					'list_owner' => 0,
784
					'list_uid' => 'group'.$account_id,
785
					'list_carddav_name' => 'group'.$account_id.'.vcf',
786
					'list_etag' => md5(json_encode($GLOBALS['egw']->accounts->members($account_id, true)))
787
				);
788
				if ($member_attr)
789
				{
790
					$list['members'] = array();	// ToDo
791
				}
792
				$lists[(string)$account_id] = $list;
793
			}
794
		}*/
795
		//error_log(__METHOD__.'('.array2string($uids).", '$uid_column', '$member_attr') returning ".array2string($lists));
796
		return $lists;
797
	}
798
799
	/**
800
	 * Adds / updates a distribution list
801
	 *
802
	 * @param string|array $keys list-name or array with column-name => value pairs to specify the list
803
	 * @param int $owner user- or group-id
804
	 * @param array $contacts =array() contacts to add (only for not yet existing lists!)
805
	 * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name'
806
	 * @return int|boolean integer list_id or false on error
807
	 */
808
	function add_list($keys,$owner,$contacts=array(),array &$data=array())
809
	{
810
		//error_log(__METHOD__.'('.array2string($keys).", $owner, ".array2string($contacts).', '.array2string($data).') '.function_backtrace());
811
		if (!$keys && !$data || !(int)$owner) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! $keys && ! $data) || ! (int)$owner, Probably Intended Meaning: ! $keys && (! $data || ! (int)$owner)
Loading history...
812
813
		if ($keys && !is_array($keys)) $keys = array('list_name' => $keys);
814
		if ($keys)
815
		{
816
			$keys['list_owner'] = $owner;
817
		}
818
		else
819
		{
820
			$data['list_owner'] = $owner;
821
		}
822
		if (!$keys || !($list_id = $this->db->select($this->lists_table,'list_id',$keys,__LINE__,__FILE__)->fetchColumn()))
823
		{
824
			$data['list_created'] = time();
825
			$data['list_creator'] = $GLOBALS['egw_info']['user']['account_id'];
826
		}
827
		else
828
		{
829
			$data[] = 'list_etag=list_etag+1';
830
		}
831
		$data['list_modified'] = time();
832
		$data['list_modifier'] = $GLOBALS['egw_info']['user']['account_id'];
833
		if (!$data['list_id']) unset($data['list_id']);
834
835
		if (!$this->db->insert($this->lists_table,$data,$keys,__LINE__,__FILE__)) return false;
836
837
		if (!$list_id && ($list_id = $this->db->get_last_insert_id($this->lists_table,'list_id')) &&
838
			(!isset($data['list_uid']) || !isset($data['list_carddav_name'])))
839
		{
840
			$update = array();
841
			if (!isset($data['list_uid']))
842
			{
843
				$update['list_uid'] = $data['list_uid'] = Api\CalDAV::generate_uid('addresbook-lists', $list_id);
844
			}
845
			if (!isset($data['list_carddav_name']))
846
			{
847
				$update['list_carddav_name'] = $data['list_carddav_name'] = $data['list_uid'].'.vcf';
848
			}
849
			$this->db->update($this->lists_table,$update,array('list_id'=>$list_id),__LINE__,__FILE__);
850
851
			$this->add2list($list_id,$contacts,array());
852
		}
853
		if ($keys) $data += $keys;
854
		//error_log(__METHOD__.'('.array2string($keys).", $owner, ...) data=".array2string($data).' returning '.array2string($list_id));
855
		return $list_id;
856
	}
857
858
	/**
859
	 * Adds contact(s) to a distribution list
860
	 *
861
	 * @param int|array $contact contact_id(s)
862
	 * @param int $list list-id
863
	 * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array()
864
	 * @return false on error
865
	 */
866
	function add2list($contact,$list,array $existing=null)
867
	{
868
		if (!(int)$list || !is_array($contact) && !(int)$contact) return false;
869
870
		if (!is_array($existing))
871
		{
872
			$existing = array();
873
			foreach($this->db->select($this->ab2list_table,'contact_id',array('list_id'=>$list),__LINE__,__FILE__) as $row)
874
			{
875
				$existing[] = $row['contact_id'];
876
			}
877
		}
878
		if (!($to_add = array_diff((array)$contact,$existing)))
879
		{
880
			return true;	// no need to insert it, would give sql error
881
		}
882
		foreach($to_add as $contact)
0 ignored issues
show
introduced by
$contact is overwriting one of the parameters of this function.
Loading history...
883
		{
884
			$this->db->insert($this->ab2list_table,array(
885
				'contact_id' => $contact,
886
				'list_id' => $list,
887
				'list_added' => time(),
888
				'list_added_by' => $GLOBALS['egw_info']['user']['account_id'],
889
			),array(),__LINE__,__FILE__);
890
		}
891
		// update etag
892
		return $this->db->update($this->lists_table,array(
893
			'list_etag=list_etag+1',
894
			'list_modified' => time(),
895
			'list_modifier' => $GLOBALS['egw_info']['user']['account_id'],
896
		),array(
897
			'list_id' => $list,
898
		),__LINE__,__FILE__);
899
	}
900
901
	/**
902
	 * Removes one contact from distribution list(s)
903
	 *
904
	 * @param int|array $contact contact_id(s)
905
	 * @param int $list =null list-id or null to remove from all lists
906
	 * @return false on error
907
	 */
908
	function remove_from_list($contact,$list=null)
909
	{
910
		if (!(int)$list && !is_null($list) || !is_array($contact) && !(int)$contact) return false;
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! (int)$list && ! is_nu...act) && ! (int)$contact, Probably Intended Meaning: ! (int)$list && (! is_nu...ct) && ! (int)$contact)
Loading history...
911
912
		$where = array(
913
			'contact_id' => $contact,
914
		);
915
		if (!is_null($list))
916
		{
917
			$where['list_id'] = $list;
918
		}
919
		else
920
		{
921
			$list = array();
922
			foreach($this->db->select($this->ab2list_table,'list_id',$where,__LINE__,__FILE__) as $row)
923
			{
924
				$list[] = $row['list_id'];
925
			}
926
		}
927
		if (!$this->db->delete($this->ab2list_table,$where,__LINE__,__FILE__))
928
		{
929
			return false;
930
		}
931
		foreach((array)$list as $list_id)
932
		{
933
			$this->db->update($this->lists_table,array(
934
				'list_etag=list_etag+1',
935
				'list_modified' => time(),
936
				'list_modifier' => $GLOBALS['egw_info']['user']['account_id'],
937
			),array(
938
				'list_id' => $list_id,
939
			),__LINE__,__FILE__);
940
		}
941
		return true;
942
	}
943
944
	/**
945
	 * Deletes a distribution list (incl. it's members)
946
	 *
947
	 * @param int|array $list list_id(s)
948
	 * @return number of members deleted or false if list does not exist
949
	 */
950
	function delete_list($list)
951
	{
952
		if (!$this->db->delete($this->lists_table,array('list_id' => $list),__LINE__,__FILE__)) return false;
953
954
		$this->db->delete($this->ab2list_table,array('list_id' => $list),__LINE__,__FILE__);
955
956
		return $this->db->affected_rows();
957
	}
958
959
	/**
960
	 * Get ctag (max list_modified as timestamp) for lists
961
	 *
962
	 * @param int|array $owner =null null for all lists user has access too
963
	 * @return int
964
	 */
965
	function lists_ctag($owner=null)
966
	{
967
		if (is_null($owner)) $owner = array_keys($this->grants);
968
969
		if (!($modified = $this->db->select($this->lists_table,'MAX(list_modified)',array('list_owner'=>$owner),
970
			__LINE__,__FILE__)->fetchColumn()))
971
		{
972
			return 0;
973
		}
974
		return $this->db->from_timestamp($modified);
975
	}
976
977
	/**
978
	 * Reads a contact, reimplemented to use the uid, if a non-numeric key is given
979
	 *
980
	 * @param int|string|array $keys
981
	 * @param string|array $extra_cols
982
	 * @param string $join
983
	 * @return array|boolean
984
	 */
985
	function read($keys,$extra_cols='',$join='')
986
	{
987
		if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) {
988
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
989
		} else {
990
			$minimum_uid_length = 8;
991
		}
992
993
		if (!is_array($keys) && !is_numeric($keys))
994
		{
995
			$keys = array('uid' => $keys);
996
		}
997
		try {
998
			$contact = parent::read($keys,$extra_cols,$join);
999
		}
1000
		// catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267)
1001
		// caused by non-ascii chars compared with ascii field uid
1002
		catch(Api\Db\Exception $e) {
1003
			_egw_log_exception($e);
1004
			return false;
1005
		}
1006
1007
		// enforce a minium uid strength
1008
		if (is_array($contact) && (!isset($contact['uid'])
1009
				|| strlen($contact['uid']) < $minimum_uid_length)) {
1010
			parent::update(array('uid' => Api\CalDAV::generate_uid('addressbook',$contact['id'])));
1011
		}
1012
		return $contact;
1013
	}
1014
1015
	/**
1016
	 * Saves a contact, reimplemented to check a given etag and set a uid
1017
	 *
1018
	 * @param array $keys if given $keys are copied to data before saveing => allows a save as
1019
	 * @param string|array $extra_where =null extra where clause, eg. to check the etag, returns 'nothing_affected' if not affected rows
1020
	 * @return int 0 on success and errno != 0 else
1021
	 */
1022
	function save($keys = NULL, $extra_where = NULL)
1023
	{
1024
		unset($extra_where);	// not used, but required by function signature
1025
1026
		if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) {
1027
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
1028
		} else {
1029
			$minimum_uid_length = 8;
1030
		}
1031
1032
		if (is_array($keys) && count($keys)) $this->data_merge($keys);
1033
1034
		$new_entry = !$this->data['id'];
1035
1036
		if (isset($this->data['etag']))		// do we have an etag in the data to write
1037
		{
1038
			$etag = $this->data['etag'];
1039
			unset($this->data['etag']);
1040
			if (!($err = parent::save(array('contact_etag=contact_etag+1'),array('contact_etag' => $etag))))
1041
			{
1042
				$this->data['etag'] = $etag+1;
1043
			}
1044
			else
1045
			{
1046
				$this->data['etag'] = $etag;
1047
			}
1048
		}
1049
		else
1050
		{
1051
			unset($this->data['etag']);
1052
			if (!($err = parent::save(array('contact_etag=contact_etag+1'))) && $new_entry)
1053
			{
1054
				$this->data['etag'] = 0;
1055
			}
1056
		}
1057
1058
		$update = array();
1059
		// enforce a minium uid strength
1060
		if (!isset($this->data['uid']) || strlen($this->data['uid']) < $minimum_uid_length)
1061
		{
1062
			$update['uid'] = Api\CalDAV::generate_uid('addressbook',$this->data['id']);
1063
			//echo "<p>set uid={$this->data['uid']}, etag={$this->data['etag']}</p>";
1064
		}
1065
		// set carddav_name, if not given by caller
1066
		if (empty($this->data['carddav_name']))
1067
		{
1068
			$update['carddav_name'] = $this->data['id'].'.vcf';
1069
		}
1070
		// update photo in entry-directory, unless hinted it is unchanged
1071
		if (!$err && $this->data['photo_unchanged'] !== true)
1072
		{
1073
			// in case files bit-field is not available read it from DB
1074
			if (!isset($this->data['files']))
1075
			{
1076
				$this->data['files'] = (int)$this->db->select($this->table_name, 'contact_files', array(
1077
					'contact_id' => $this->data['id'],
1078
				), __LINE__, __FILE__)->fetchColumn();
1079
			}
1080
			$path =  Api\Link::vfs_path('addressbook', $this->data['id'], Api\Contacts::FILES_PHOTO);
1081
			$backup = Api\Vfs::$is_root; Api\Vfs::$is_root = true;
1082
			if (empty($this->data['jpegphoto']))
1083
			{
1084
				unlink($path);
1085
				$update['files'] = $this->data['files'] & ~Api\Contacts::FILES_BIT_PHOTO;
1086
			}
1087
			else
1088
			{
1089
				file_put_contents($path, $this->data['jpegphoto']);
1090
				$update['files'] = $this->data['files'] | Api\Contacts::FILES_BIT_PHOTO;
1091
			}
1092
			Api\Vfs::$is_root = $backup;
1093
		}
1094
		if (!$err && $update)
1095
		{
1096
			parent::update($update);
1097
		}
1098
		return $err;
1099
	}
1100
1101
1102
	/**
1103
	 * Read data of a distribution list
1104
	 *
1105
	 * @param int $list list_id
1106
	 * @return array of data or false if list does not exist
1107
	 */
1108
	function read_list($list)
1109
	{
1110
		if (!$list) return false;
1111
1112
		return $this->db->select($this->lists_table,'*',array('list_id'=>$list),__LINE__,__FILE__)->fetch();
1113
	}
1114
1115
	/**
1116
	 * saves custom field data
1117
	 * Re-implemented to deal with extra contact_owner column
1118
	 *
1119
	 * @param array $data data to save (cf's have to be prefixed with self::CF_PREFIX = #)
1120
	 * @param array $extra_cols =array() extra-data to be saved
1121
	 * @return bool false on success, errornumber on failure
1122
	 */
1123
	function save_customfields(&$data, array $extra_cols=array())
1124
	{
1125
		return parent::save_customfields($data, array('contact_owner' => $data['owner'])+$extra_cols);
1126
	}
1127
1128
	/**
1129
	* Deletes custom field data
1130
	* Implemented to deal with LDAP backend, which saves CFs in SQL, but the account record is in LDAP
1131
	*/
1132
	function delete_customfields($data)
1133
	{
1134
		$this->db->delete($this->extra_table,$data,__LINE__,__FILE__);
1135
	}
1136
}
1137