Completed
Push — 16.1 ( 277864...22528c )
by Ralf
24:04 queued 06:02
created

Storage::migrate2ldap()   F

Complexity

Conditions 22
Paths 380

Size

Total Lines 104
Code Lines 60

Duplication

Lines 11
Ratio 10.58 %

Importance

Changes 0
Metric Value
cc 22
eloc 60
nc 380
nop 1
dl 11
loc 104
rs 3.5977
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

1
<?php
2
/**
3
 * EGroupware API - Contacts storage object
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Cornelius Weiss <egw-AT-von-und-zu-weiss.de>
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @package addressbook
9
 * @copyright (c) 2005-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @copyright (c) 2005/6 by Cornelius Weiss <[email protected]>
11
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
12
 * @version $Id$
13
 */
14
15
namespace EGroupware\Api\Contacts;
16
17
use EGroupware\Api;
18
19
/**
20
 * Contacts storage object
21
 *
22
 * The contact storage has 3 operation modi (contact_repository):
23
 * - sql: contacts are stored in the SQL table egw_addressbook & egw_addressbook_extra (custom fields)
24
 * - ldap: contacts are stored in LDAP (accounts have to be stored in LDAP too!!!).
25
 *   Custom fields are not availible in that case!
26
 * - sql-ldap: contacts are read and searched in SQL, but saved to both SQL and LDAP.
27
 *   Other clients (Thunderbird, ...) can use LDAP readonly. The get maintained via eGroupWare only.
28
 *
29
 * The accounts can be stored in SQL or LDAP too (account_repository):
30
 * If the account-repository is different from the contacts-repository, the filter all (no owner set)
31
 * will only search the contacts and NOT the accounts! Only the filter accounts (owner=0) shows accounts.
32
 *
33
 * If sql-ldap is used as contact-storage (LDAP is managed from eGroupWare) the filter all, searches
34
 * the accounts in the SQL contacts-table too. Change in made in LDAP, are not detected in that case!
35
 */
36
37
class Storage
38
{
39
	/**
40
	 * name of customefields table
41
	 *
42
	 * @var string
43
	 */
44
	var $extra_table = 'egw_addressbook_extra';
45
46
	/**
47
	* @var string
48
	*/
49
	var $extra_id = 'contact_id';
50
51
	/**
52
	* @var string
53
	*/
54
	var $extra_owner = 'contact_owner';
55
56
	/**
57
	* @var string
58
	*/
59
	var $extra_key = 'contact_name';
60
61
	/**
62
	* @var string
63
	*/
64
	var $extra_value = 'contact_value';
65
66
	/**
67
	 * view for distributionlistsmembership
68
	 *
69
	 * @var string
70
	 */
71
	var $distributionlist_view ='(SELECT contact_id, egw_addressbook_lists.list_id as list_id, egw_addressbook_lists.list_name as list_name, egw_addressbook_lists.list_owner as list_owner FROM egw_addressbook_lists, egw_addressbook2list where egw_addressbook_lists.list_id=egw_addressbook2list.list_id) d_view ';
72
	var $distributionlist_tabledef = array();
73
	/**
74
	* @var string
75
	*/
76
	var $distri_id = 'contact_id';
77
78
	/**
79
	* @var string
80
	*/
81
	var $distri_owner = 'list_owner';
82
83
	/**
84
	* @var string
85
	*/
86
	var $distri_key = 'list_id';
87
88
	/**
89
	* @var string
90
	*/
91
	var $distri_value = 'list_name';
92
93
	/**
94
	 * Contact repository in 'sql' or 'ldap'
95
	 *
96
	 * @var string
97
	 */
98
	var $contact_repository = 'sql';
99
100
	/**
101
	 * Grants as  account_id => rights pairs
102
	 *
103
	 * @var array
104
	 */
105
	var $grants;
106
107
	/**
108
	 *  userid of current user
109
	 *
110
	 * @var int
111
	 */
112
	var $user;
113
114
	/**
115
	 * memberships of the current user
116
	 *
117
	 * @var array
118
	 */
119
	var $memberships;
120
121
	/**
122
	 * In SQL we can search all columns, though a view make on real sense
123
	 */
124
	var $sql_cols_not_to_search = array(
125
		'jpegphoto','owner','tid','private','cat_id','etag',
126
		'modified','modifier','creator','created','tz','account_id',
127
		'uid','carddav_name','freebusy_uri','calendar_uri',
128
		'geo','pubkey',
129
	);
130
	/**
131
	 * columns to search, if we search for a single pattern
132
	 *
133
	 * @var array
134
	 */
135
	var $columns_to_search = array();
136
	/**
137
	 * extra columns to search if accounts are included, eg. account_lid
138
	 *
139
	 * @var array
140
	 */
141
	var $account_extra_search = array();
142
	/**
143
	 * columns to search for accounts, if stored in different repository
144
	 *
145
	 * @var array
146
	 */
147
	var $account_cols_to_search = array();
148
149
	/**
150
	 * customfields name => array(...) pairs
151
	 *
152
	 * @var array
153
	 */
154
	var $customfields = array();
155
	/**
156
	 * content-types as name => array(...) pairs
157
	 *
158
	 * @var array
159
	 */
160
	var $content_types = array();
161
162
	/**
163
	* Special content type to indicate a deleted addressbook
164
	*
165
	* @var String;
166
	*/
167
	const DELETED_TYPE = 'D';
168
169
	/**
170
	 * total number of matches of last search
171
	 *
172
	 * @var int
173
	 */
174
	var $total;
175
176
	/**
177
	 * storage object: sql (Sql) or ldap (addressbook_ldap) backend class
178
	 *
179
	 * @var Sql
180
	 */
181
	var $somain;
182
	/**
183
	 * storage object for accounts, if not identical to somain (eg. accounts in ldap, contacts in sql)
184
	 *
185
	 * @var Ldap
186
	 */
187
	var $so_accounts;
188
	/**
189
	 * account repository sql or ldap
190
	 *
191
	 * @var string
192
	 */
193
	var $account_repository = 'sql';
194
	/**
195
	 * custom fields backend
196
	 *
197
	 * @var Sql
198
	 */
199
	var $soextra;
200
	var $sodistrib_list;
201
202
	/**
203
	 * Constructor
204
	 *
205
	 * @param string $contact_app ='addressbook' used for acl->get_grants()
206
	 * @param Api\Db $db =null
207
	 */
208
	function __construct($contact_app='addressbook',Api\Db $db=null)
209
	{
210
		$this->db     = is_null($db) ? $GLOBALS['egw']->db : $db;
211
212
		$this->user = $GLOBALS['egw_info']['user']['account_id'];
213
		$this->memberships = $GLOBALS['egw']->accounts->memberships($this->user,true);
214
215
		// account backend used
216 View Code Duplication
		if ($GLOBALS['egw_info']['server']['account_repository'])
217
		{
218
			$this->account_repository = $GLOBALS['egw_info']['server']['account_repository'];
219
		}
220
		elseif ($GLOBALS['egw_info']['server']['auth_type'])
221
		{
222
			$this->account_repository = $GLOBALS['egw_info']['server']['auth_type'];
223
		}
224
		$this->customfields = Api\Storage\Customfields::get('addressbook');
225
		// contacts backend (contacts in LDAP require accounts in LDAP!)
226
		if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap')
227
		{
228
			$this->contact_repository = 'ldap';
229
			$this->somain = new Ldap();
230
			$this->columns_to_search = $this->somain->search_attributes;
231
		}
232
		else	// sql or sql->ldap
233
		{
234
			if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap')
235
			{
236
				$this->contact_repository = 'sql-ldap';
237
			}
238
			$this->somain = new Sql($db);
239
240
			// remove some columns, absolutly not necessary to search in sql
241
			$this->columns_to_search = array_diff(array_values($this->somain->db_cols),$this->sql_cols_not_to_search);
242
			if ($this->customfields)	// add custom fields, if configured
243
			{
244
				$this->columns_to_search[] = Sql::EXTRA_TABLE.'.'.Sql::EXTRA_VALUE;
245
			}
246
		}
247
		if ($this->user)
248
		{
249
			$this->grants = $this->get_grants($this->user,$contact_app);
250
		}
251
		if ($this->account_repository != 'sql' && $this->contact_repository == 'sql')
252
		{
253
			if ($this->account_repository != $this->contact_repository)
254
			{
255
				$class = 'EGroupware\\Api\\Contacts\\'.ucfirst($this->account_repository);
256
				$this->so_accounts = new $class();
257
				$this->account_cols_to_search = $this->so_accounts->search_attributes;
258
			}
259
			else
260
			{
261
				$this->account_extra_search = array('uid');
262
			}
263
		}
264
		if ($this->contact_repository == 'sql' || $this->contact_repository == 'sql-ldap')
265
		{
266
			$tda2list = $this->db->get_table_definitions('api','egw_addressbook2list');
267
			$tdlists = $this->db->get_table_definitions('api','egw_addressbook_lists');
268
			$this->distributionlist_tabledef = array('fd' => array(
269
					$this->distri_id => $tda2list['fd'][$this->distri_id],
270
					$this->distri_owner => $tdlists['fd'][$this->distri_owner],
271
        	    	$this->distri_key => $tdlists['fd'][$this->distri_key],
272
					$this->distri_value => $tdlists['fd'][$this->distri_value],
273
				), 'pk' => array(), 'fk' => array(), 'ix' => array(), 'uc' => array(),
274
			);
275
		}
276
		// ToDo: it should be the other way arround, the backend should set the grants it uses
277
		$this->somain->grants =& $this->grants;
278
279
		if($this->somain instanceof Sql)
280
		{
281
			$this->soextra =& $this->somain;
282
		}
283
		else
284
		{
285
			$this->soextra = new Sql($db);
286
		}
287
288
		$this->content_types = Api\Config::get_content_types('addressbook');
289
		if (!$this->content_types)
290
		{
291
			$this->content_types = array('n' => array(
292
				'name' => 'contact',
293
				'options' => array(
294
					'template' => 'addressbook.edit',
295
					'icon' => 'navbar.png'
296
			)));
297
		}
298
299
		// Add in deleted type, if holding deleted contacts
300
		$config = Api\Config::read('phpgwapi');
301
		if($config['history'])
302
		{
303
			$this->content_types[self::DELETED_TYPE] = array(
304
				'name'	=>	lang('Deleted'),
305
				'options' =>	array(
306
					'template'	=>	'addressbook.edit',
307
					'icon'		=>	'deleted.png'
308
				)
309
			);
310
		}
311
	}
312
313
	/**
314
	 * Get grants for a given user, taking into account static LDAP ACL
315
	 *
316
	 * @param int $user
317
	 * @param string $contact_app ='addressbook'
318
	 * @return array
319
	 */
320
	function get_grants($user, $contact_app='addressbook', $preferences=null)
321
	{
322
		if (!isset($preferences)) $preferences = $GLOBALS['egw_info']['user']['preferences'];
323
324
		if ($user)
325
		{
326
			// contacts backend (contacts in LDAP require accounts in LDAP!)
327
			if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap')
328
			{
329
				// static grants from ldap: all rights for the own personal addressbook and the group ones of the meberships
330
				$grants = array($user => ~0);
331
				foreach($GLOBALS['egw']->accounts->memberships($user,true) as $gid)
332
				{
333
					$grants[$gid] = ~0;
334
				}
335
			}
336
			else	// sql or sql->ldap
337
			{
338
				// group grants are now grants for the group addressbook and NOT grants for all its members,
339
				// therefor the param false!
340
				$grants = $GLOBALS['egw']->acl->get_grants($contact_app,false,$user);
341
			}
342
			// add grants for accounts: if account_selection not in ('none','groupmembers'): everyone has read access,
343
			// if he has not set the hide_accounts preference
344
			// ToDo: be more specific for 'groupmembers', they should be able to see the groupmembers
345
			if (!in_array($preferences['common']['account_selection'], array('none','groupmembers')))
346
			{
347
				$grants[0] = Api\Acl::READ;
348
			}
349
			// add account grants for admins (only for current user!)
350
			if ($user == $this->user && $this->is_admin())	// admin rights can be limited by ACL!
351
			{
352
				$grants[0] = Api\Acl::READ;	// admins always have read-access
353 View Code Duplication
				if (!$GLOBALS['egw']->acl->check('account_access',16,'admin')) $grants[0] |= Api\Acl::EDIT;
354 View Code Duplication
				if (!$GLOBALS['egw']->acl->check('account_access',4,'admin'))  $grants[0] |= Api\Acl::ADD;
355 View Code Duplication
				if (!$GLOBALS['egw']->acl->check('account_access',32,'admin')) $grants[0] |= Api\Acl::DELETE;
356
			}
357
			// allow certain groups to edit contact-data of accounts
358
			if (self::allow_account_edit($user))
359
			{
360
				$grants[0] |= Api\Acl::READ|Api\Acl::EDIT;
361
			}
362
		}
363
		else
364
		{
365
			$grants = array();
366
		}
367
		//error_log(__METHOD__."($user, '$contact_app') returning ".array2string($grants));
368
		return $grants;
369
	}
370
371
	/**
372
	 * Check if the user is an admin (can unconditionally edit accounts)
373
	 *
374
	 * We check now the admin ACL for edit users, as the admin app does it for editing accounts.
375
	 *
376
	 * @param array $contact =null for future use, where admins might not be admins for all accounts
377
	 * @return boolean
378
	 */
379
	function is_admin($contact=null)
380
	{
381
		unset($contact);	// not (yet) used
382
383
		return isset($GLOBALS['egw_info']['user']['apps']['admin']) && !$GLOBALS['egw']->acl->check('account_access',16,'admin');
384
	}
385
386
	/**
387
	 * Check if current user is in a group, which is allowed to edit accounts
388
	 *
389
	 * @param int $user =null default $this->user
390
	 * @return boolean
391
	 */
392
	function allow_account_edit($user=null)
393
	{
394
		return $GLOBALS['egw_info']['server']['allow_account_edit'] &&
395
			array_intersect($GLOBALS['egw_info']['server']['allow_account_edit'],
396
				$GLOBALS['egw']->accounts->memberships($user ? $user : $this->user, true));
397
	}
398
399
	/**
400
	 * Read all customfields of the given id's
401
	 *
402
	 * @param int|array $ids
403
	 * @param array $field_names =null custom fields to read, default all
404
	 * @return array id => name => value
405
	 */
406
	function read_customfields($ids,$field_names=null)
407
	{
408
		return $this->soextra->read_customfields($ids,$field_names);
409
	}
410
411
	/**
412
	 * Read all distributionlists of the given id's
413
	 *
414
	 * @param int|array $ids
415
	 * @return array id => name => value
416
	 */
417
	function read_distributionlist($ids, $dl_allowed=array())
418
	{
419
		if ($this->contact_repository == 'ldap')
420
		{
421
			return array();	// ldap does not support distributionlists
422
		}
423
		foreach($ids as $key => $id)
0 ignored issues
show
Bug introduced by
The expression $ids of type integer|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
424
		{
425
			if (!is_numeric($id)) unset($ids[$key]);
426
		}
427
		if (!$ids) return array();	// nothing to do, eg. all these contacts are in ldap
428
		$fields = array();
429
		$filter[$this->distri_id]=$ids;
430
		if (count($dl_allowed)) $filter[$this->distri_key]=$dl_allowed;
431
		$distri_view = str_replace(') d_view',' and '.$this->distri_id.' in ('.implode(',',$ids).')) d_view',$this->distributionlist_view);
432
		#_debug_array($this->distributionlist_tabledef);
433
		foreach($this->db->select($distri_view, '*', $filter, __LINE__, __FILE__,
434
			false, 'ORDER BY '.$this->distri_id, false, 0, '', $this->distributionlist_tabledef) as $row)
435
		{
436
			if ((isset($row[$this->distri_id])&&strlen($row[$this->distri_value])>0))
437
			{
438
				$fields[$row[$this->distri_id]][$row[$this->distri_key]] = $row[$this->distri_value].' ('.
439
					Api\Accounts::username($row[$this->distri_owner]).')';
440
			}
441
		}
442
		return $fields;
443
	}
444
445
	/**
446
	 * changes the data from the db-format to your work-format
447
	 *
448
	 * it gets called everytime when data is read from the db
449
	 * This function needs to be reimplemented in the derived class
450
	 *
451
	 * @param array $data
452
	 */
453
	function db2data($data)
454
	{
455
		return $data;
456
	}
457
458
	/**
459
	 * changes the data from your work-format to the db-format
460
	 *
461
	 * It gets called everytime when data gets writen into db or on keys for db-searches
462
	 * this needs to be reimplemented in the derived class
463
	 *
464
	 * @param array $data
465
	 */
466
	function data2db($data)
467
	{
468
		return $data;
469
	}
470
471
	/**
472
	* deletes contact entry including custom fields
473
	*
474
	* @param mixed $contact array with id or just the id
475
	* @param int $check_etag =null
476
	* @return boolean|int true on success or false on failiure, 0 if etag does not match
477
	*/
478
	function delete($contact,$check_etag=null)
479
	{
480
		if (is_array($contact)) $contact = $contact['id'];
481
482
		$where = array('id' => $contact);
483
		if ($check_etag) $where['etag'] = $check_etag;
484
485
		// delete mainfields
486
		if ($this->somain->delete($where))
487
		{
488
			// delete customfields, can return 0 if there are no customfields
489
			if(!($this->somain instanceof Sql))
490
			{
491
				$this->soextra->delete_customfields(array($this->extra_id => $contact));
492
			}
493
494
			// delete from distribution list(s)
495
			$this->remove_from_list($contact);
496
497
			if ($this->contact_repository == 'sql-ldap')
498
			{
499 View Code Duplication
				if ($contact['account_id'])
500
				{
501
					// LDAP uses the uid attributes for the contact-id (dn),
502
					// which need to be the account_lid for accounts!
503
					$contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']);
504
				}
505
				(new Ldap())->delete($contact);
506
			}
507
			return true;
508
		}
509
		return $check_etag ? 0 : false;		// if etag given, we return 0 on failure, thought it could also mean the whole contact does not exist
510
	}
511
512
	/**
513
	* saves contact data including custom fields
514
	*
515
	* @param array &$contact contact data from etemplate::exec
516
	* @return bool false on success, errornumber on failure
517
	*/
518
	function save(&$contact)
519
	{
520
		// save mainfields
521
		if ($contact['id'] && $this->contact_repository != $this->account_repository && is_object($this->so_accounts) &&
522
			($this->contact_repository == 'sql' && !is_numeric($contact['id']) ||
523
			 $this->contact_repository == 'ldap' && is_numeric($contact['id'])))
524
		{
525
			$this->so_accounts->data = $this->data2db($contact);
526
			$error_nr = $this->so_accounts->save();
527
			$contact['id'] = $this->so_accounts->data['id'];
528
		}
529
		else
530
		{
531
			// contact_repository sql-ldap (accounts in ldap) the person_id is the uid (account_lid)
532
			// for the sql write here we need to find out the existing contact_id
533
			if ($this->contact_repository == 'sql-ldap' && $contact['id'] && !is_numeric($contact['id']) &&
534
				$contact['account_id'] && ($old = $this->somain->read(array('account_id' => $contact['account_id']))))
535
			{
536
				$contact['id'] = $old['id'];
537
			}
538
			$this->somain->data = $this->data2db($contact);
539
540
			if (!($error_nr = $this->somain->save()))
541
			{
542
				$contact['id'] = $this->somain->data['id'];
543
				$contact['uid'] = $this->somain->data['uid'];
544
				$contact['etag'] = $this->somain->data['etag'];
545
546
				if ($this->contact_repository == 'sql-ldap')
547
				{
548
					$data = $this->somain->data;
549 View Code Duplication
					if ($contact['account_id'])
550
					{
551
						// LDAP uses the uid attributes for the contact-id (dn),
552
						// which need to be the account_lid for accounts!
553
						$data['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']);
554
					}
555
					$error_nr = (new Ldap())->save($data);
556
				}
557
			}
558
		}
559
		if($error_nr) return $error_nr;
560
561
		return false;	// no error
562
	}
563
564
	/**
565
	 * reads contact data including custom fields
566
	 *
567
	 * @param int|string $contact_id contact_id or 'a'.account_id
568
	 * @return array|boolean data if row could be retrived else False
569
	*/
570
	function read($contact_id)
571
	{
572
		if (!is_array($contact_id) && substr($contact_id,0,8) == 'account:')
573
		{
574
			$contact_id = array('account_id' => (int) substr($contact_id,8));
575
		}
576
		// read main data
577
		$backend =& $this->get_backend($contact_id);
578
		if (!($contact = $backend->read($contact_id)))
579
		{
580
			return $contact;
581
		}
582
		$dl_list=$this->read_distributionlist(array($contact['id']));
583
		if (count($dl_list)) $contact['distrib_lists']=implode("\n",$dl_list[$contact['id']]);
584
		return $this->db2data($contact);
585
	}
586
587
	/**
588
	 * searches db for rows matching searchcriteria
589
	 *
590
	 * '*' and '?' are replaced with sql-wildcards '%' and '_'
591
	 *
592
	 * @param array|string $criteria array of key and data cols, OR string to search over all standard search fields
593
	 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return
594
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
595
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
596
	 * @param string $wildcard ='' appended befor and after each criteria
597
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
598
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
599
	 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num)
600
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
601
	 *  $filter['cols_to_search'] limit search columns to given columns, otherwise $this->columns_to_search is used
602
	 * @param string $join ='' sql to do a join (only used by sql backend!), eg. " RIGHT JOIN egw_accounts USING(account_id)"
603
	 * @param boolean $ignore_acl =false true: no acl check
604
	 * @return array of matching rows (the row is an array of the cols) or False
605
	 */
606
	function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='', $ignore_acl=false)
607
	{
608
		//error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')");
609
610
		// Handle 'None' country option
611
		if(is_array($filter) && $filter['adr_one_countrycode'] == '-custom-')
612
		{
613
			$filter[] = 'adr_one_countrycode IS NULL';
614
			unset($filter['adr_one_countrycode']);
615
		}
616
		// Hide deleted items unless type is specifically deleted
617
		if(!is_array($filter)) $filter = $filter ? (array) $filter : array();
618
619
		if (isset($filter['cols_to_search']))
620
		{
621
			$cols_to_search = $filter['cols_to_search'];
622
			unset($filter['cols_to_search']);
623
		}
624
625
		// if no tid set or tid==='' do NOT return deleted entries ($tid === null returns all entries incl. deleted)
626
		if(!array_key_exists('tid', $filter) || $filter['tid'] === '')
627
		{
628
			if ($join && strpos($join,'RIGHT JOIN') !== false)	// used eg. to search for groups
629
			{
630
				$filter[] = '(contact_tid != \'' . self::DELETED_TYPE . '\' OR contact_tid IS NULL)';
631
			}
632
			else
633
			{
634
				$filter[] = 'contact_tid != \'' . self::DELETED_TYPE . '\'';
635
			}
636
		}
637
		elseif(is_null($filter['tid']))
638
		{
639
			unset($filter['tid']);	// return all entries incl. deleted
640
		}
641
		$backend = $this->get_backend(null,$filter['owner']);
642
		// single string to search for --> create so_sql conformant search criterial for the standard search columns
643
		if ($criteria && !is_array($criteria))
644
		{
645
			$op = 'OR';
646
			$wildcard = '%';
647
			$search = $criteria;
648
			$criteria = array();
649
650
			if (isset($cols_to_search))
651
			{
652
				$cols = $cols_to_search;
653
			}
654
			elseif ($backend === $this->somain)
655
			{
656
				$cols = $this->columns_to_search;
657
			}
658
			else
659
			{
660
				$cols = $this->account_cols_to_search;
661
			}
662
			if($backend instanceof Sql)
663
			{
664
				// Keep a string, let the parent handle it
665
				$criteria = $search;
666
667
				foreach($cols as $key => &$col)
668
				{
669
					if($col != Sql::EXTRA_VALUE &&
670
						$col != Sql::EXTRA_TABLE.'.'.Sql::EXTRA_VALUE &&
671
						!array_key_exists($col, $backend->db_cols))
672
					{
673
						if(!($col = array_search($col, $backend->db_cols)))
674
						{
675
							// Can't search this column, it will error if we try
676
							unset($cols[$key]);
677
						}
678
					}
679
					if ($col=='contact_id') $col='egw_addressbook.contact_id';
680
				}
681
682
				$backend->columns_to_search = $cols;
683
			}
684
			else
685
			{
686
				foreach($cols as $col)
687
				{
688
					// remove from LDAP backend not understood use-AND-syntax
689
					$criteria[$col] = str_replace(' +',' ',$search);
690
				}
691
			}
692
		}
693
		if (is_array($criteria) && count($criteria))
694
		{
695
			$criteria = $this->data2db($criteria);
696
		}
697
		if (is_array($filter) && count($filter))
698
		{
699
			$filter = $this->data2db($filter);
700
		}
701
		else
702
		{
703
			$filter = $filter ? array($filter) : array();
704
		}
705
		// get the used backend for the search and call it's search method
706
		$rows = $backend->search($criteria, $only_keys, $order_by, $extra_cols,
707
			$wildcard, $empty, $op, $start, $filter, $join, false, $ignore_acl);
708
709
		$this->total = $backend->total;
0 ignored issues
show
Documentation Bug introduced by
It seems like $backend->total can also be of type boolean. However, the property $total is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
710
711
		if ($rows)
712
		{
713
			foreach($rows as $n => $row)
0 ignored issues
show
Bug introduced by
The expression $rows of type boolean|object<EGroupwar...\Db2DataIterator>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
714
			{
715
				$rows[$n] = $this->db2data($row);
716
			}
717
		}
718
		return $rows;
719
	}
720
721
	/**
722
	 * Query organisations by given parameters
723
	 *
724
	 * @var array $param
725
	 * @var string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group
726
	 * @var int $param[owner] addressbook to search
727
	 * @var string $param[search] search pattern for org_name
728
	 * @var string $param[searchletter] letter the org_name need to start with
729
	 * @var int $param[start]
730
	 * @var int $param[num_rows]
731
	 * @var string $param[sort] ASC or DESC
732
	 * @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit
733
	 */
734
	function organisations($param)
735
	{
736
		if (!method_exists($this->somain,'organisations'))
737
		{
738
			$this->total = 0;
739
			return false;
740
		}
741
		if ($param['search'] && !is_array($param['search']))
742
		{
743
			$search = $param['search'];
744
			$param['search'] = array();
745
			if($this->somain instanceof Sql)
746
			{
747
				// Keep the string, let the parent deal with it
748
				$param['search'] = $search;
749
			}
750
			else
751
			{
752
				foreach($this->columns_to_search as $col)
753
				{
754
					if ($col != 'contact_value') $param['search'][$col] = $search;	// we dont search the customfields
755
				}
756
			}
757
		}
758
		if (is_array($param['search']) && count($param['search']))
759
		{
760
			$param['search'] = $this->data2db($param['search']);
761
		}
762
		if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '')
763
		{
764
			$param['col_filter'][] = 'contact_tid != \'' . self::DELETED_TYPE . '\'';
765
		}
766
		elseif(is_null($param['col_filter']['tid']))
767
		{
768
			unset($param['col_filter']['tid']);	// return all entries incl. deleted
769
		}
770
771
		$rows = $this->somain->organisations($param);
772
		$this->total = $this->somain->total;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->somain->total can also be of type boolean. However, the property $total is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
773
774
		if (!$rows) return array();
775
776
		foreach($rows as $n => $row)
777
		{
778
			if (strpos($row['org_name'],'&')!==false) $row['org_name'] = str_replace('&','*AND*',$row['org_name']);
779
			$rows[$n]['id'] = 'org_name:'.$row['org_name'];
780
			foreach(array(
781
				'org_unit' => lang('departments'),
782
				'adr_one_locality' => lang('locations'),
783
			) as $by => $by_label)
784
			{
785
				if ($row[$by.'_count'] > 1)
786
				{
787
					$rows[$n][$by] = $row[$by.'_count'].' '.$by_label;
788
				}
789
				else
790
				{
791
					if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]);
792
					$rows[$n]['id'] .= '|||'.$by.':'.$row[$by];
793
				}
794
			}
795
		}
796
		return $rows;
797
	}
798
799
 	/**
800
	 * gets all contact fields from database
801
	 *
802
	 * @return array of (internal) field-names
803
	 */
804
	function get_contact_columns()
805
	{
806
		$fields = $this->get_fields('all');
807
		foreach (array_keys((array)$this->customfields) as $cfield)
808
		{
809
			$fields[] = '#'.$cfield;
810
		}
811
		return $fields;
812
	}
813
814
	/**
815
	 * delete / move all contacts of an addressbook
816
	 *
817
	 * @param array $data
818
	 * @param int $data['account_id'] owner to change
819
	 * @param int $data['new_owner']  new owner or 0 for delete
820
	 */
821
	function deleteaccount($data)
822
	{
823
		$account_id = $data['account_id'];
824
		$new_owner =  $data['new_owner'];
825
826
		if (!$new_owner)
827
		{
828
			$this->somain->delete(array('owner' => $account_id));	// so_sql_cf::delete() takes care of cfs too
829
830
			if (method_exists($this->somain, 'get_lists') &&
831
				($lists = $this->somain->get_lists($account_id)))
832
			{
833
				$this->somain->delete_list(array_keys($lists));
834
			}
835
		}
836
		else
837
		{
838
			$this->somain->change_owner($account_id,$new_owner);
839
		}
840
	}
841
842
	/**
843
	 * return the backend, to be used for the given $contact_id
844
	 *
845
	 * @param array|string|int $keys =null
846
	 * @param int $owner =null account_id of owner or 0 for accounts
847
	 * @return Sql
848
	 */
849
	function get_backend($keys=null,$owner=null)
850
	{
851
		if ($owner === '') $owner = null;
852
853
		$contact_id = !is_array($keys) ? $keys :
854
			(isset($keys['id']) ? $keys['id'] : $keys['contact_id']);
855
856
		if ($this->contact_repository != $this->account_repository && is_object($this->so_accounts) &&
857
			(!is_null($owner) && !$owner || is_array($keys) && $keys['account_id'] || !is_null($contact_id) &&
858
			($this->contact_repository == 'sql' && (!is_numeric($contact_id) && !is_array($contact_id) )||
859
			 $this->contact_repository == 'ldap' && is_numeric($contact_id))))
860
		{
861
			return $this->so_accounts;
862
		}
863
		return $this->somain;
864
	}
865
866
	/**
867
	 * Returns the supported, all or unsupported fields of the backend (depends on owner or contact_id)
868
	 *
869
	 * @param sting $type ='all' 'supported', 'unsupported' or 'all'
870
	 * @param mixed $contact_id =null
871
	 * @param int $owner =null account_id of owner or 0 for accounts
872
	 * @return array with eGW contact field names
873
	 */
874
	function get_fields($type='all',$contact_id=null,$owner=null)
875
	{
876
		$def = $this->db->get_table_definitions('api','egw_addressbook');
877
878
		$all_fields = array();
879
		foreach(array_keys($def['fd']) as $field)
880
		{
881
			$all_fields[] = substr($field,0,8) == 'contact_' ? substr($field,8) : $field;
882
		}
883
		if ($type == 'all')
884
		{
885
			return $all_fields;
886
		}
887
		$backend =& $this->get_backend($contact_id,$owner);
888
889
		$supported_fields = method_exists($backend,supported_fields) ? $backend->supported_fields() : $all_fields;
890
891
		if ($type == 'supported')
892
		{
893
			return $supported_fields;
894
		}
895
		return array_diff($all_fields,$supported_fields);
896
	}
897
898
	/**
899
	 * Migrates an SQL contact storage to LDAP, SQL-LDAP or back to SQL
900
	 *
901
	 * @param string|array $type comma-separated list or array of:
902
	 *  - "contacts" contacts to ldap
903
	 *  - "accounts" accounts to ldap
904
	 *  - "accounts-back" accounts back to sql (for sql-ldap!)
905
	 *  - "sql" contacts and accounts to sql
906
	 *  - "accounts-back-ads" accounts back from ads to sql
907
	 */
908
	function migrate2ldap($type)
909
	{
910
		//error_log(__METHOD__."(".array2string($type).")");
911
		$sql_contacts  = new Sql();
912
		if ($type == 'accounts-back-ads')
913
		{
914
			$ldap_contacts = new Ads();
915
		}
916
		else
917
		{
918
			// we need an admin connection
919
			$ds = $GLOBALS['egw']->ldap->ldapConnect();
920
			$ldap_contacts = new Ldap(null, $ds);
921
		}
922
923
		if (!is_array($type)) $type = explode(',', $type);
924
925
		$start = $n = 0;
926
		$num = 100;
927
928
		// direction SQL --> LDAP, either only accounts, or only contacts or both
929
		if (($do = array_intersect($type, array('contacts', 'accounts'))))
930
		{
931
			$filter = count($do) == 2 ? null :
932
				array($do[0] == 'contacts' ? 'contact_owner != 0' : 'contact_owner = 0');
933
934
			while (($contacts = $sql_contacts->search(false,false,'n_family,n_given','','',false,'AND',
935
				array($start,$num),$filter)))
936
			{
937
				foreach($contacts as $contact)
0 ignored issues
show
Bug introduced by
The expression $contacts of type boolean|object<EGroupwar...\Db2DataIterator>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
938
				{
939 View Code Duplication
					if ($contact['account_id']) $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']);
940
941
					$ldap_contacts->data = $contact;
942
					$n++;
943
					if (!($err = $ldap_contacts->save()))
944
					{
945
						echo '<p style="margin: 0px;">'.$n.': '.$contact['n_fn'].
946
							($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> LDAP</p>\n";
947
					}
948 View Code Duplication
					else
949
					{
950
						echo '<p style="margin: 0px; color: red;">'.$n.': '.$contact['n_fn'].
951
							($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."</p>\n";
952
					}
953
				}
954
				$start += $num;
955
			}
956
		}
957
		// direction LDAP --> SQL: either "sql" (contacts and accounts) or "accounts-back" (only accounts)
958
		if (($do = array_intersect(array('accounts-back','sql'), $type)))
959
		{
960
			//error_log(__METHOD__."(".array2string($type).") do=".array2string($type));
961
			$filter = in_array('sql', $do) ? null : array('owner' => 0);
962
963
			foreach($ldap_contacts->search(false,false,'n_family,n_given','','',false,'AND',
0 ignored issues
show
Bug introduced by
The expression $ldap_contacts->search(f... 'AND', false, $filter) of type false|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
964
				false, $filter) as $contact)
965
			{
966
				//error_log(__METHOD__."(".array2string($type).") do=".array2string($type)." migrating ".array2string($contact));
967
				if ($contact['jpegphoto'])	// photo is NOT read by LDAP backend on search, need to do an extra read
968
				{
969
					$contact = $ldap_contacts->read($contact['id']);
970
				}
971
				$old_contact_id = $contact['id'];
972
				unset($contact['id']);	// ldap uid/account_lid
973
				if ($contact['account_id'] && ($old = $sql_contacts->read(array('account_id' => $contact['account_id']))))
974
				{
975
					$contact['id'] = $old['id'];
976
				}
977
				$sql_contacts->data = $contact;
978
979
				$n++;
980
				if (!($err = $sql_contacts->save()))
981
				{
982
					echo '<p style="margin: 0px;">'.$n.': '.$contact['n_fn'].
983
						($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> SQL (".
984
						($contact['owner']?lang('User'):lang('Contact')).")<br>\n";
985
986
					$new_contact_id = $sql_contacts->data['id'];
987
					echo "&nbsp;&nbsp;&nbsp;&nbsp;" . $old_contact_id . " --> " . $new_contact_id . " / ";
988
989
					$tq = $this->db->update('egw_links',array(
990
						'link_id1' => $new_contact_id,
991
					),array(
992
						'link_app1' => 'addressbook',
993
						'link_id1' => $old_contact_id
994
					),__LINE__,__FILE__);
995
996
					$tq = $this->db->update('egw_links',array(
997
						'link_id2' => $new_contact_id,
998
					),array(
999
						'link_app2' => 'addressbook',
1000
						'link_id2' => $old_contact_id
1001
					),__LINE__,__FILE__);
1002
					echo "</p>\n";
1003
				}
1004 View Code Duplication
				else
1005
				{
1006
					echo '<p style="margin: 0px; color: red;">'.$n.': '.$contact['n_fn'].
1007
						($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."</p>\n";
1008
				}
1009
			}
1010
		}
1011
	}
1012
1013
	/**
1014
	 * Get the availible distribution lists for a user
1015
	 *
1016
	 * @param int $required =Api\Acl::READ required rights on the list or multiple rights or'ed together,
1017
	 * 	to return only lists fullfilling all the given rights
1018
	 * @param string $extra_labels =null first labels if given (already translated)
1019
	 * @return array with id => label pairs or false if backend does not support lists
1020
	 */
1021
	function get_lists($required=Api\Acl::READ,$extra_labels=null)
1022
	{
1023
		$lists = is_array($extra_labels) ? $extra_labels : array();
1024
1025
		if (method_exists($this->somain,'get_lists'))
1026
		{
1027
			$uids = array();
1028
			foreach($this->grants as $uid => $rights)
1029
			{
1030
				// only requests groups / list in accounts addressbook for read
1031
				if (!$uid && $required != Api\Acl::READ) continue;
1032
1033
				if (($rights & $required) == $required)
1034
				{
1035
					$uids[] = $uid;
1036
				}
1037
			}
1038
1039
			foreach($this->somain->get_lists($uids) as $list_id => $data)
1040
			{
1041
				$lists[$list_id] = $data['list_name'];
1042
				if ($data['list_owner'] != $this->user)
1043
				{
1044
					$lists[$list_id] .= ' ('.Api\Accounts::username($data['list_owner']).')';
1045
				}
1046
			}
1047
		}
1048
1049
		// add groups for all backends, if accounts addressbook is not hidden
1050
		if (empty($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']))
1051
		{
1052
			foreach($GLOBALS['egw']->accounts->search(array(
1053
				'type' => 'groups'
1054
			)) as $account_id => $group)
1055
			{
1056
				$lists[(string)$account_id] = Api\Accounts::format_username($group['account_lid'], '', '', $account_id);
1057
			}
1058
		}
1059
1060
		return $lists;
1061
	}
1062
1063
	/**
1064
	 * Get the availible distribution lists for givens users and groups
1065
	 *
1066
	 * @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid)
1067
	 * @param string $member_attr ='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute
1068
	 * @param boolean $limit_in_ab =false if true only return members from the same owners addressbook
1069
	 * @return array with list_id => array(list_id,list_name,list_owner,...) pairs
1070
	 */
1071
	function read_lists($keys,$member_attr=null,$limit_in_ab=false)
1072
	{
1073
		$backend = (string)$limit_in_ab === '0' && $this->so_accounts ? $this->so_accounts : $this->somain;
1074
		if (!method_exists($backend, 'get_lists')) return false;
1075
1076
		return $backend->get_lists($keys,null,$member_attr,$limit_in_ab);
1077
	}
1078
1079
	/**
1080
	 * Adds / updates a distribution list
1081
	 *
1082
	 * @param string|array $keys list-name or array with column-name => value pairs to specify the list
1083
	 * @param int $owner user- or group-id
1084
	 * @param array $contacts =array() contacts to add (only for not yet existing lists!)
1085
	 * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name'
1086
	 * @return int|boolean integer list_id or false on error
1087
	 */
1088
	function add_list($keys,$owner,$contacts=array(),array &$data=array())
1089
	{
1090
		$backend = (string)$owner === '0' && $this->so_accounts ? $this->so_accounts : $this->somain;
1091
		if (!method_exists($backend, 'add_list')) return false;
1092
1093
		return $backend->add_list($keys,$owner,$contacts,$data);
1094
	}
1095
1096
	/**
1097
	 * Adds contact(s) to a distribution list
1098
	 *
1099
	 * @param int|array $contact contact_id(s)
1100
	 * @param int $list list-id
1101
	 * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array()
1102
	 * @return false on error
1103
	 */
1104
	function add2list($contact,$list,array $existing=null)
1105
	{
1106
		if (!method_exists($this->somain,'add2list')) return false;
1107
1108
		return $this->somain->add2list($contact,$list,$existing);
1109
	}
1110
1111
	/**
1112
	 * Removes one contact from distribution list(s)
1113
	 *
1114
	 * @param int|array $contact contact_id(s)
1115
	 * @param int $list =null list-id or null to remove from all lists
1116
	 * @return false on error
1117
	 */
1118
	function remove_from_list($contact,$list=null)
1119
	{
1120
		if (!method_exists($this->somain,'remove_from_list')) return false;
1121
1122
		return $this->somain->remove_from_list($contact,$list);
1123
	}
1124
1125
	/**
1126
	 * Deletes a distribution list (incl. it's members)
1127
	 *
1128
	 * @param int|array $list list_id(s)
1129
	 * @return number of members deleted or false if list does not exist
1130
	 */
1131
	function delete_list($list)
1132
	{
1133
		if (!method_exists($this->somain,'delete_list')) return false;
1134
1135
		return $this->somain->delete_list($list);
1136
	}
1137
1138
	/**
1139
	 * Read data of a distribution list
1140
	 *
1141
	 * @param int $list list_id
1142
	 * @return array of data or false if list does not exist
1143
	 */
1144
	function read_list($list)
1145
	{
1146
		if (!method_exists($this->somain,'read_list')) return false;
1147
1148
		return $this->somain->read_list($list);
1149
	}
1150
1151
	/**
1152
	 * Check if distribution lists are availible for a given addressbook
1153
	 *
1154
	 * @param int|string $owner ='' addressbook (eg. 0 = accounts), default '' = "all" addressbook (uses the main backend)
1155
	 * @return boolean
1156
	 */
1157
	function lists_available($owner='')
1158
	{
1159
		$backend =& $this->get_backend(null,$owner);
1160
1161
		return method_exists($backend,'read_list');
1162
	}
1163
1164
	/**
1165
	 * Get ctag (max list_modified as timestamp) for lists
1166
	 *
1167
	 * @param int|array $owner =null null for all lists user has access too
1168
	 * @return int
1169
	 */
1170
	function lists_ctag($owner=null)
1171
	{
1172
		if (!method_exists($this->somain,'lists_ctag')) return 0;
1173
1174
		return $this->somain->lists_ctag($owner);
1175
	}
1176
}
1177