Sql   F
last analyzed

Complexity

Total Complexity 232

Size/Duplication

Total Lines 1114
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 486
dl 0
loc 1114
rs 2
c 0
b 0
f 0
wmc 232

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
F save() 0 77 17
D add_list() 0 48 18
C get_lists() 0 60 13
A change_owner() 0 26 2
A delete_list() 0 7 2
F organisations() 0 162 42
B add2list() 0 33 8
B remove_from_list() 0 34 9
A read_list() 0 5 2
A lists_ctag() 0 10 3
F duplicates() 0 153 33
A save_customfields() 0 3 1
A _cat_search() 0 8 5
A delete_customfields() 0 3 1
A _cat_filter() 0 16 4
F search() 0 173 60
B read() 0 28 8

How to fix   Complexity   

Complex Class

Complex classes like Sql often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Sql, and based on these observations, apply Extract Interface, too.

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