Contacts   F
last analyzed

Complexity

Total Complexity 585

Size/Duplication

Total Lines 2635
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1150
dl 0
loc 2635
rs 1
c 1
b 0
f 0
wmc 585

49 Methods

Rating   Name   Duplication   Size   Complexity  
A fileas_type() 0 15 6
C fileas() 0 47 14
F __construct() 0 153 18
A cf_options() 0 8 2
A fileas_options() 0 23 4
B db2data() 0 25 11
F get_addressbooks() 0 58 20
A private_addressbook() 0 3 2
A fullname() 0 13 5
D set_all_cleanup() 0 82 19
C set_all_fileas() 0 50 15
B read() 0 35 9
A remove_from_list() 0 14 4
A delete_list() 0 10 3
F read_org() 0 107 28
B read_calendar() 0 22 7
C link_query() 0 44 12
A fixup_contact() 0 10 3
D addr_format_by_country() 0 57 43
A photo_src() 0 9 5
C read_birthdays() 0 51 14
A deleteaccount() 0 4 1
A file_access() 0 5 1
A link_titles() 0 25 6
A read_list() 0 5 2
B changed_fields() 0 24 8
A calendar_info() 0 18 6
B link_title() 0 30 11
B get_categories() 0 22 8
C delete() 0 48 17
C photo() 0 60 14
B read_calendar_type() 0 79 11
A add2list() 0 7 2
A add_list() 0 14 4
A resize_photo() 0 36 6
C change_org() 0 50 14
A org_similar() 0 18 4
A get_ctag() 0 25 6
A clear_birthday_cache() 0 6 2
C find_or_add_categories() 0 53 15
A check_list() 0 8 3
A delete_category() 0 24 6
A merge_calendar() 0 18 3
F save() 0 198 75
A data2db() 0 11 3
A link_query_email() 0 13 3
F find_contact() 0 220 51
D check_perms() 0 50 24
F merge() 0 116 35

How to fix   Complexity   

Complex Class

Complex classes like Contacts 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 Contacts, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * EGroupware API: Contacts
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Cornelius Weiss <[email protected]>
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Joerg Lehrke <[email protected]>
9
 * @package api
10
 * @subpackage contacts
11
 * @copyright (c) 2005-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
12
 * @copyright (c) 2005/6 by Cornelius Weiss <[email protected]>
13
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
14
 * @version $Id$
15
 */
16
17
namespace EGroupware\Api;
18
19
use calendar_bo;	// to_do: do NOT require it, just use if there
20
21
/**
22
 * Business object for contacts
23
 */
24
class Contacts extends Contacts\Storage
25
{
26
27
	/**
28
	 * Birthdays are read into the cache, cache is expired when a
29
	 * birthday changes, or after 10 days.
30
	 */
31
	const BIRTHDAY_CACHE_TIME = 864000; /* 10 days*/
32
33
	/**
34
	 * @var int $now_su actual user (!) time
35
	 */
36
	var $now_su;
37
38
	/**
39
	 * @var array $timestamps timestamps
40
	 */
41
	var $timestamps = array('modified','created');
42
43
	/**
44
	 * @var array $fileas_types
45
	 */
46
	var $fileas_types = array(
47
		'org_name: n_family, n_given',
48
		'org_name: n_family, n_prefix',
49
		'org_name: n_given n_family',
50
		'org_name: n_fn',
51
		'org_name, org_unit: n_family, n_given',
52
		'org_name, adr_one_locality: n_family, n_given',
53
		'org_name, org_unit, adr_one_locality: n_family, n_given',
54
		'n_family, n_given: org_name',
55
		'n_family, n_given (org_name)',
56
		'n_family, n_prefix: org_name',
57
		'n_given n_family: org_name',
58
		'n_prefix n_family: org_name',
59
		'n_fn: org_name',
60
		'org_name',
61
		'org_name - org_unit',
62
		'n_given n_family',
63
		'n_prefix n_family',
64
		'n_family, n_given',
65
		'n_family, n_prefix',
66
		'n_fn',
67
		'n_family, n_given (bday)',
68
	);
69
70
	/**
71
	 * @var array $org_fields fields belonging to the (virtual) organisation entry
72
	 */
73
	var $org_fields = array(
74
		'org_name',
75
		'org_unit',
76
		'adr_one_street',
77
		'adr_one_street2',
78
		'adr_one_locality',
79
		'adr_one_region',
80
		'adr_one_postalcode',
81
		'adr_one_countryname',
82
		'adr_one_countrycode',
83
		'label',
84
		'tel_work',
85
		'tel_fax',
86
		'tel_assistent',
87
		'assistent',
88
		'email',
89
		'url',
90
		'tz',
91
	);
92
93
	/**
94
	 * Which fields is a (non-admin) user allowed to edit in his own account
95
	 *
96
	 * @var array
97
	 */
98
	var $own_account_acl;
99
100
	/**
101
	 * @var double $org_common_factor minimum percentage of the contacts with identical values to construct the "common" (virtual) org-entry
102
	 */
103
	var $org_common_factor = 0.6;
104
105
	var $contact_fields = array();
106
	var $business_contact_fields = array();
107
	var $home_contact_fields = array();
108
109
	/**
110
	 * Set Logging
111
	 *
112
	 * @var boolean
113
	 */
114
	var $log = false;
115
	var $logfile = '/tmp/log-addressbook_bo';
116
117
	/**
118
	 * Number and message of last error or false if no error, atm. only used for saving
119
	 *
120
	 * @var string/boolean
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/boolean at position 0 could not be parsed: Unknown type name 'string/boolean' at position 0 in string/boolean.
Loading history...
121
	 */
122
	var $error;
123
	/**
124
	 * Addressbook preferences of the user
125
	 *
126
	 * @var array
127
	 */
128
	var $prefs;
129
	/**
130
	 * Default addressbook for new contacts, if no addressbook is specified (user preference)
131
	 *
132
	 * @var int
133
	 */
134
	var $default_addressbook;
135
	/**
136
	 * Default addressbook is the private one
137
	 *
138
	 * @var boolean
139
	 */
140
	var $default_private;
141
	/**
142
	 * Use a separate private addressbook (former private flag), for contacts not shareable via regular read acl
143
	 *
144
	 * @var boolean
145
	 */
146
	var $private_addressbook = false;
147
	/**
148
	 * Categories object
149
	 *
150
	 * @var Categories
151
	 */
152
	var $categories;
153
154
	/**
155
	* Tracking changes
156
	*
157
	* @var Contacts\Tracking
158
	*/
159
	protected $tracking;
160
161
	/**
162
	* Keep deleted addresses, or really delete them
163
	* Set in Admin -> Addressbook -> Site Configuration
164
	* ''=really delete, 'history'=keep, only admins delete, 'userpurge'=keep, users delete
165
 	*
166
	* @var string
167
 	*/
168
	protected $delete_history = '';
169
170
	/**
171
	 * Constructor
172
	 *
173
	 * @param string $contact_app ='addressbook' used for acl->get_grants()
174
	 * @param Db $db =null
175
	 */
176
	function __construct($contact_app='addressbook',Db $db=null)
177
	{
178
		parent::__construct($contact_app,$db);
179
		if ($this->log)
180
		{
181
			$this->logfile = $GLOBALS['egw_info']['server']['temp_dir'].'/log-addressbook_bo';
182
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($contact_app)\n", 3 ,$this->logfile);
183
		}
184
185
		$this->now_su = DateTime::to('now','ts');
0 ignored issues
show
Documentation Bug introduced by
It seems like EGroupware\Api\DateTime::to('now', 'ts') of type EGroupware\Api\datetime is incompatible with the declared type integer of property $now_su.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
186
187
		$this->prefs =& $GLOBALS['egw_info']['user']['preferences']['addressbook'];
188
		if(!isset($this->prefs['hide_accounts']))
189
		{
190
			$this->prefs['hide_accounts'] = '0';
191
		}
192
		// get the default addressbook from the users prefs
193
		$this->default_addressbook = $GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] ?
194
			(int)$GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] : $this->user;
195
		$this->default_private = substr($GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'],-1) == 'p';
196
		if ($this->default_addressbook > 0 && $this->default_addressbook != $this->user &&
197
			($this->default_private ||
198
			$this->default_addressbook == (int)$GLOBALS['egw']->preferences->forced['addressbook']['add_default'] ||
199
			$this->default_addressbook == (int)$GLOBALS['egw']->preferences->default['addressbook']['add_default']))
200
		{
201
			$this->default_addressbook = $this->user;	// admin set a default or forced pref for personal addressbook
202
		}
203
		$this->private_addressbook = self::private_addressbook($this->contact_repository == 'sql', $this->prefs);
204
205
		$this->contact_fields = array(
206
			'id'                   => lang('Contact ID'),
207
			'tid'                  => lang('Type'),
208
			'owner'                => lang('Addressbook'),
209
			'private'              => lang('private'),
210
			'cat_id'               => lang('Category'),
211
			'n_prefix'             => lang('prefix'),
212
			'n_given'              => lang('first name'),
213
			'n_middle'             => lang('middle name'),
214
			'n_family'             => lang('last name'),
215
			'n_suffix'             => lang('suffix'),
216
			'n_fn'                 => lang('full name'),
217
			'n_fileas'             => lang('own sorting'),
218
			'bday'                 => lang('birthday'),
219
			'org_name'             => lang('Organisation'),
220
			'org_unit'             => lang('Department'),
221
			'title'                => lang('title'),
222
			'role'                 => lang('role'),
223
			'assistent'            => lang('Assistent'),
224
			'room'                 => lang('Room'),
225
			'adr_one_street'       => lang('business street'),
226
			'adr_one_street2'      => lang('business address line 2'),
227
			'adr_one_locality'     => lang('business city'),
228
			'adr_one_region'       => lang('business state'),
229
			'adr_one_postalcode'   => lang('business zip code'),
230
			'adr_one_countryname'  => lang('business country'),
231
			'adr_one_countrycode'  => lang('business country code'),
232
			'label'                => lang('label'),
233
			'adr_two_street'       => lang('street (private)'),
234
			'adr_two_street2'      => lang('address line 2 (private)'),
235
			'adr_two_locality'     => lang('city (private)'),
236
			'adr_two_region'       => lang('state (private)'),
237
			'adr_two_postalcode'   => lang('zip code (private)'),
238
			'adr_two_countryname'  => lang('country (private)'),
239
			'adr_two_countrycode'  => lang('country code (private)'),
240
			'tel_work'             => lang('work phone'),
241
			'tel_cell'             => lang('mobile phone'),
242
			'tel_fax'              => lang('business fax'),
243
			'tel_assistent'        => lang('assistent phone'),
244
			'tel_car'              => lang('car phone'),
245
			'tel_pager'            => lang('pager'),
246
			'tel_home'             => lang('home phone'),
247
			'tel_fax_home'         => lang('fax (private)'),
248
			'tel_cell_private'     => lang('mobile phone (private)'),
249
			'tel_other'            => lang('other phone'),
250
			'tel_prefer'           => lang('preferred phone'),
251
			'email'                => lang('business email'),
252
			'email_home'           => lang('email (private)'),
253
			'url'                  => lang('url (business)'),
254
			'url_home'             => lang('url (private)'),
255
			'freebusy_uri'         => lang('Freebusy URI'),
256
			'calendar_uri'         => lang('Calendar URI'),
257
			'note'                 => lang('note'),
258
			'tz'                   => lang('time zone'),
259
			'geo'                  => lang('geo'),
260
			'pubkey'               => lang('public key'),
261
			'created'              => lang('created'),
262
			'creator'              => lang('created by'),
263
			'modified'             => lang('last modified'),
264
			'modifier'             => lang('last modified by'),
265
			'jpegphoto'            => lang('photo'),
266
			'account_id'           => lang('Account ID'),
267
		);
268
		$this->business_contact_fields = array(
269
			'org_name'             => lang('Company'),
270
			'org_unit'             => lang('Department'),
271
			'title'                => lang('Title'),
272
			'role'                 => lang('Role'),
273
			'n_prefix'             => lang('prefix'),
274
			'n_given'              => lang('first name'),
275
			'n_middle'             => lang('middle name'),
276
			'n_family'             => lang('last name'),
277
			'n_suffix'             => lang('suffix'),
278
			'adr_one_street'       => lang('street').' ('.lang('business').')',
279
			'adr_one_street2'      => lang('address line 2').' ('.lang('business').')',
280
			'adr_one_locality'     => lang('city').' ('.lang('business').')',
281
			'adr_one_region'       => lang('state').' ('.lang('business').')',
282
			'adr_one_postalcode'   => lang('zip code').' ('.lang('business').')',
283
			'adr_one_countryname'  => lang('country').' ('.lang('business').')',
284
		);
285
		$this->home_contact_fields = array(
286
			'org_name'             => lang('Company'),
287
			'org_unit'             => lang('Department'),
288
			'title'                => lang('Title'),
289
			'role'                 => lang('Role'),
290
			'n_prefix'             => lang('prefix'),
291
			'n_given'              => lang('first name'),
292
			'n_middle'             => lang('middle name'),
293
			'n_family'             => lang('last name'),
294
			'n_suffix'             => lang('suffix'),
295
			'adr_two_street'       => lang('street').' ('.lang('business').')',
296
			'adr_two_street2'      => lang('address line 2').' ('.lang('business').')',
297
			'adr_two_locality'     => lang('city').' ('.lang('business').')',
298
			'adr_two_region'       => lang('state').' ('.lang('business').')',
299
			'adr_two_postalcode'   => lang('zip code').' ('.lang('business').')',
300
			'adr_two_countryname'  => lang('country').' ('.lang('business').')',
301
		);
302
		//_debug_array($this->contact_fields);
303
		$this->own_account_acl = $GLOBALS['egw_info']['server']['own_account_acl'];
304
		if (!is_array($this->own_account_acl)) $this->own_account_acl = json_php_unserialize($this->own_account_acl, true);
0 ignored issues
show
Documentation Bug introduced by
It seems like json_php_unserialize($th...>own_account_acl, true) can also be of type false or string. However, the property $own_account_acl is declared as type array. 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...
305
		// we have only one acl (n_fn) for the whole name, as not all backends store every part in an own field
306
		if ($this->own_account_acl && in_array('n_fn',$this->own_account_acl))
307
		{
308
			$this->own_account_acl = array_merge($this->own_account_acl,array('n_prefix','n_given','n_middle','n_family','n_suffix'));
309
		}
310
		if ($GLOBALS['egw_info']['server']['org_fileds_to_update'])
311
		{
312
			$this->org_fields =  $GLOBALS['egw_info']['server']['org_fileds_to_update'];
313
			if (!is_array($this->org_fields)) $this->org_fields = unserialize($this->org_fields);
314
315
			// Set country code if country name is selected
316
			$supported_fields = $this->get_fields('supported',null,0);
317
			if(in_array('adr_one_countrycode', $supported_fields) && in_array('adr_one_countryname',$this->org_fields))
318
			{
319
				$this->org_fields[] = 'adr_one_countrycode';
320
			}
321
			if(in_array('adr_two_countrycode', $supported_fields) && in_array('adr_two_countryname',$this->org_fields))
322
			{
323
				$this->org_fields[] = 'adr_two_countrycode';
324
			}
325
		}
326
		$this->categories = new Categories($this->user,'addressbook');
327
328
		$this->delete_history = $GLOBALS['egw_info']['server']['history'];
329
	}
330
331
	/**
332
	 * Do we use a private addressbook (in comparison to a personal one)
333
	 *
334
	 * Used to set $this->private_addressbook for current user.
335
	 *
336
	 * @param string $contact_repository
337
	 * @param array $prefs addressbook preferences
338
	 * @return boolean
339
	 */
340
	public static function private_addressbook($contact_repository, array $prefs=null)
341
	{
342
		return $contact_repository == 'sql' && $prefs['private_addressbook'];
343
	}
344
345
	/**
346
	 * Get the availible addressbooks of the user
347
	 *
348
	 * @param int $required =Acl::READ required rights on the addressbook or multiple rights or'ed together,
349
	 * 	to return only addressbooks fullfilling all the given rights
350
	 * @param string $extra_label first label if given (already translated)
351
	 * @param int $user =null account_id or null for current user
352
	 * @return array with owner => label pairs
353
	 */
354
	function get_addressbooks($required=Acl::READ,$extra_label=null,$user=null)
355
	{
356
		if (is_null($user))
357
		{
358
			$user = $this->user;
359
			$preferences = $GLOBALS['egw_info']['user']['preferences'];
360
			$grants = $this->grants;
361
		}
362
		else
363
		{
364
			$prefs_obj = new Preferences($user);
365
			$preferences = $prefs_obj->read_repository();
366
			$grants = $this->get_grants($user, 'addressbook', $preferences);
367
		}
368
369
		$addressbooks = $to_sort = array();
370
		if ($extra_label) $addressbooks[''] = $extra_label;
371
		$addressbooks[$user] = lang('Personal');
372
		// add all group addressbooks the user has the necessary rights too
373
		foreach($grants as $uid => $rights)
374
		{
375
			if (($rights & $required) == $required && $GLOBALS['egw']->accounts->get_type($uid) == 'g')
376
			{
377
				$to_sort[$uid] = lang('Group %1',$GLOBALS['egw']->accounts->id2name($uid));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $GLOBALS['egw']->accounts->id2name($uid). ( Ignorable by Annotation )

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

377
				$to_sort[$uid] = /** @scrutinizer ignore-call */ lang('Group %1',$GLOBALS['egw']->accounts->id2name($uid));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
378
			}
379
		}
380
		if ($to_sort)
381
		{
382
			asort($to_sort);
383
			$addressbooks += $to_sort;
384
		}
385
		if ($required != Acl::ADD &&	// do NOT allow to set accounts as default addressbook (AB can add accounts)
386
			$preferences['addressbook']['hide_accounts'] !== '1' && (
387
				($grants[0] & $required) == $required ||
388
				$preferences['common']['account_selection'] == 'groupmembers' &&
389
				$this->account_repository != 'ldap' && ($required & Acl::READ)))
390
		{
391
			$addressbooks[0] = lang('Accounts');
392
		}
393
		// add all other user addressbooks the user has the necessary rights too
394
		$to_sort = array();
395
		foreach($grants as $uid => $rights)
396
		{
397
			if ($uid != $user && ($rights & $required) == $required && $GLOBALS['egw']->accounts->get_type($uid) == 'u')
398
			{
399
				$to_sort[$uid] = Accounts::username($uid);
400
			}
401
		}
402
		if ($to_sort)
403
		{
404
			asort($to_sort);
405
			$addressbooks += $to_sort;
406
		}
407
		if ($user > 0 && self::private_addressbook($this->contact_repository, $preferences['addressbook']))
408
		{
409
			$addressbooks[$user.'p'] = lang('Private');
410
		}
411
		return $addressbooks;
412
	}
413
414
	/**
415
	 * calculate the file_as string from the contact and the file_as type
416
	 *
417
	 * @param array $contact
418
	 * @param string $type =null file_as type, default null to read it from the contact, unknown/not set type default to the first one
419
	 * @param boolean $isUpdate =false If true, reads the old record for any not set fields
420
	 * @return string
421
	 */
422
	function fileas($contact,$type=null, $isUpdate=false)
423
	{
424
		if (is_null($type)) $type = $contact['fileas_type'];
425
		if (!$type) $type = $this->prefs['fileas_default'] ? $this->prefs['fileas_default'] : $this->fileas_types[0];
426
427
		if (strpos($type,'n_fn') !== false) $contact['n_fn'] = $this->fullname($contact);
428
429
		if($isUpdate)
430
		{
431
			$fileas_fields = array('n_prefix','n_given','n_middle','n_family','n_suffix','n_fn','org_name','org_unit','adr_one_locality','bday');
432
			$old = null;
433
			foreach($fileas_fields as $field)
434
			{
435
				if(!isset($contact[$field]))
436
				{
437
					if(is_null($old)) $old = $this->read($contact['id']);
438
					$contact[$field] = $old[$field];
439
				}
440
			}
441
			unset($old);
442
		}
443
444
		// removing empty delimiters, caused by empty contact fields
445
		$fileas = str_replace(array(', , : ',', : ',': , ',', , ',': : ',' ()'),
446
			array(': ',': ',': ',', ',': ',''),
447
			strtr($type, array(
448
				'n_prefix' => $contact['n_prefix'],
449
				'n_given'  => $contact['n_given'],
450
				'n_middle' => $contact['n_middle'],
451
				'n_family' => $contact['n_family'],
452
				'n_suffix' => $contact['n_suffix'],
453
				'n_fn'     => $contact['n_fn'],
454
				'org_name' => $contact['org_name'],
455
				'org_unit' => $contact['org_unit'],
456
				'adr_one_locality' => $contact['adr_one_locality'],
457
				'bday'     => (int)$contact['bday'] ? DateTime::to($contact['bday'], true) : $contact['bday'],
458
			)));
459
460
		while ($fileas[0] == ':' ||  $fileas[0] == ',')
461
		{
462
			$fileas = substr($fileas,2);
463
		}
464
		while (substr($fileas,-2) == ': ' || substr($fileas,-2) == ', ')
465
		{
466
			$fileas = substr($fileas,0,-2);
467
		}
468
		return $fileas;
469
	}
470
471
	/**
472
	 * determine the file_as type from the file_as string and the contact
473
	 *
474
	 * @param array $contact
475
	 * @param string $file_as =null file_as type, default null to read it from the contact, unknown/not set type default to the first one
476
	 * @return string
477
	 */
478
	function fileas_type($contact,$file_as=null)
479
	{
480
		if (is_null($file_as)) $file_as = $contact['n_fileas'];
481
482
		if ($file_as)
483
		{
484
			foreach($this->fileas_types as $type)
485
			{
486
				if ($this->fileas($contact,$type) == $file_as)
487
				{
488
					return $type;
489
				}
490
			}
491
		}
492
		return $this->prefs['fileas_default'] ? $this->prefs['fileas_default'] : $this->fileas_types[0];
493
	}
494
495
	/**
496
	 * get selectbox options for the customfields
497
	 *
498
	 * @param array $field =null
499
	 * @return array with options:
500
	 */
501
	public static function cf_options()
502
	{
503
		$cf_fields = Storage\Customfields::get('addressbook',TRUE);
504
		foreach ($cf_fields as $key => $value )
505
		{
506
			$options[$key]= $value['label'];
507
		}
508
		return $options;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $options seems to be defined by a foreach iteration on line 504. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
509
	}
510
511
	/**
512
	 * get selectbox options for the fileas types with translated labels, or real content
513
	 *
514
	 * @param array $contact =null real content to use, default none
515
	 * @return array with options: fileas type => label pairs
516
	 */
517
	function fileas_options($contact=null)
518
	{
519
		$labels = array(
520
			'n_prefix' => lang('prefix'),
521
			'n_given'  => lang('first name'),
522
			'n_middle' => lang('middle name'),
523
			'n_family' => lang('last name'),
524
			'n_suffix' => lang('suffix'),
525
			'n_fn'     => lang('full name'),
526
			'org_name' => lang('company'),
527
			'org_unit' => lang('department'),
528
			'adr_one_locality' => lang('city'),
529
			'bday'     => lang('Birthday'),
530
		);
531
		foreach(array_keys($labels) as $name)
532
		{
533
			if ($contact[$name]) $labels[$name] = $contact[$name];
534
		}
535
		foreach($this->fileas_types as $fileas_type)
536
		{
537
			$options[$fileas_type] = $this->fileas($labels,$fileas_type);
538
		}
539
		return $options;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $options seems to be defined by a foreach iteration on line 535. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
540
	}
541
542
	/**
543
	 * Set n_fileas (and n_fn) in contacts of all users  (called by Admin >> Addressbook >> Site configuration (Admin only)
544
	 *
545
	 * If $all all fileas fields will be set, if !$all only empty ones
546
	 *
547
	 * @param string $fileas_type '' or type of $this->fileas_types
548
	 * @param int $all =false update all contacts or only ones with empty values
549
	 * @param int &$errors=null on return number of errors
550
	 * @return int|boolean number of contacts updated, false for wrong fileas type
551
	 */
552
	function set_all_fileas($fileas_type,$all=false,&$errors=null,$ignore_acl=false)
553
	{
554
		if ($fileas_type != '' && !in_array($fileas_type, $this->fileas_types))
555
		{
556
			return false;
557
		}
558
		if ($ignore_acl)
559
		{
560
			unset($this->somain->grants);	// to NOT limit search to contacts readable by current user
561
		}
562
		// to be able to work on huge contact repositories we read the contacts in chunks of 100
563
		for($n = $updated = $errors = 0; ($contacts = parent::search($all ? array() : array(
564
			'n_fileas IS NULL',
565
			"n_fileas=''",
566
			'n_fn IS NULL',
567
			"n_fn=''",
568
		),false,'','','',false,'OR',array($n*100,100))); ++$n)
569
		{
570
			foreach($contacts as $contact)
571
			{
572
				$old_fn     = $contact['n_fn'];
573
				$old_fileas = $contact['n_fileas'];
574
				$contact['n_fn'] = $this->fullname($contact);
575
				// only update fileas if type is given AND (all should be updated or n_fileas is empty)
576
				if ($fileas_type && ($all || empty($contact['n_fileas'])))
0 ignored issues
show
Bug Best Practice introduced by
The expression $all of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
577
				{
578
					$contact['n_fileas'] = $this->fileas($contact,$fileas_type);
579
				}
580
				if ($old_fileas != $contact['n_fileas'] || $old_fn != $contact['n_fn'])
581
				{
582
					// only specify/write updated fields plus "keys"
583
					$contact = array_intersect_key($contact,array(
584
						'id' => true,
585
						'owner' => true,
586
						'private' => true,
587
						'account_id' => true,
588
						'uid' => true,
589
					)+($old_fileas != $contact['n_fileas'] ? array('n_fileas' => true) : array())+($old_fn != $contact['n_fn'] ? array('n_fn' => true) : array()));
590
					if ($this->save($contact,$ignore_acl))
591
					{
592
						$updated++;
593
					}
594
					else
595
					{
596
						$errors++;
597
					}
598
				}
599
			}
600
		}
601
		return $updated;
602
	}
603
604
	/**
605
	 * Cleanup all contacts db fields of all users  (called by Admin >> Addressbook >> Site configuration (Admin only)
606
	 *
607
	 * Cleanup means to truncate all unnecessary chars like whitespaces or tabs,
608
	 * remove unneeded carriage returns or set empty fields to NULL
609
	 *
610
	 * @param int &$errors=null on return number of errors
611
	 * @return int|boolean number of contacts updated
612
	 */
613
	function set_all_cleanup(&$errors=null,$ignore_acl=false)
614
	{
615
		if ($ignore_acl)
616
		{
617
			unset($this->somain->grants);	// to NOT limit search to contacts readable by current user
618
		}
619
620
		// fields that must not be touched
621
		$fields_exclude = array(
622
			'id'			=> true,
623
			'tid'			=> true,
624
			'owner'			=> true,
625
			'private'		=> true,
626
			'created'		=> true,
627
			'creator'		=> true,
628
			'modified'		=> true,
629
			'modifier'		=> true,
630
			'account_id'	=> true,
631
			'etag'			=> true,
632
			'uid'			=> true,
633
			'freebusy_uri'	=> true,
634
			'calendar_uri'	=> true,
635
			'photo'			=> true,
636
		);
637
638
		// to be able to work on huge contact repositories we read the contacts in chunks of 100
639
		for($n = $updated = $errors = 0; ($contacts = parent::search(array(),false,'','','',false,'OR',array($n*100,100))); ++$n)
640
		{
641
			foreach($contacts as $contact)
642
			{
643
				$fields_to_update = array();
644
				foreach($contact as $field_name => $field_value)
645
				{
646
					if($fields_exclude[$field_name] === true) continue; // dont touch specified field
647
648
					if (is_string($field_value) && $field_name != 'pubkey' && $field_name != 'jpegphoto')
649
					{
650
						// check if field has to be trimmed
651
						if (strlen($field_value) != strlen(trim($field_value)))
652
						{
653
							$fields_to_update[$field_name] = $field_value = trim($field_value);
654
						}
655
						// check if field contains a carriage return - exclude notes
656
						if ($field_name != 'note' && strpos($field_value,"\x0D\x0A") !== false)
657
						{
658
							$fields_to_update[$field_name] = $field_value = str_replace("\x0D\x0A"," ",$field_value);
659
						}
660
					}
661
					// check if a field contains an empty string
662
					if (is_string($field_value) && strlen($field_value) == 0)
663
					{
664
						$fields_to_update[$field_name] = $field_value = null;
665
					}
666
					// check for valid birthday date
667
					if ($field_name == 'bday' && $field_value != null &&
668
						!preg_match('/^(18|19|20|21|22)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/',$field_value))
669
					{
670
						$fields_to_update[$field_name] = $field_value = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $field_value is dead and can be removed.
Loading history...
671
					}
672
				}
673
674
				if(count($fields_to_update) > 0)
675
				{
676
					$contact_to_save = array(
677
						'id' => $contact['id'],
678
						'owner' => $contact['owner'],
679
						'private' => $contact['private'],
680
						'account_id' => $contact['account_id'],
681
						'uid' => $contact['uid']) + $fields_to_update;
682
683
					if ($this->save($contact_to_save,$ignore_acl))
684
					{
685
						$updated++;
686
					}
687
					else
688
					{
689
						$errors++;
690
					}
691
				}
692
			}
693
		}
694
		return $updated;
695
	}
696
697
	/**
698
	 * get full name from the name-parts
699
	 *
700
	 * @param array $contact
701
	 * @return string full name
702
	 */
703
	function fullname($contact)
704
	{
705
		if (empty($contact['n_family']) && empty($contact['n_given'])) {
706
			$cpart = array('org_name');
707
		} else {
708
			$cpart = array('n_prefix','n_given','n_middle','n_family','n_suffix');
709
		}
710
		$parts = array();
711
		foreach($cpart as $n)
712
		{
713
			if ($contact[$n]) $parts[] = $contact[$n];
714
		}
715
		return implode(' ',$parts);
716
	}
717
718
	/**
719
	 * changes the data from the db-format to your work-format
720
	 *
721
	 * it gets called everytime when data is read from the db
722
	 * This function needs to be reimplemented in the derived class
723
	 *
724
	 * @param array $data
725
	 * @param $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
0 ignored issues
show
Documentation Bug introduced by
The doc comment ='ts' at position 0 could not be parsed: Unknown type name '='ts'' at position 0 in ='ts'.
Loading history...
726
	 *
727
	 * @return array updated data
728
	 */
729
	function db2data($data, $date_format='ts')
730
	{
731
		static $fb_url = false;
732
733
		// convert timestamps from server-time in the db to user-time
734
		foreach ($this->timestamps as $name)
735
		{
736
			if (isset($data[$name]))
737
			{
738
				$data[$name] = DateTime::server2user($data[$name], $date_format);
739
			}
740
		}
741
		$data['photo'] = $this->photo_src($data['id'],$data['jpegphoto'] || ($data['files'] & self::FILES_BIT_PHOTO), '', $data['etag']);
742
743
		// set freebusy_uri for accounts
744
		if (!$data['freebusy_uri'] && !$data['owner'] && $data['account_id'] && !is_object($GLOBALS['egw_setup']))
745
		{
746
			if ($fb_url || @is_dir(EGW_SERVER_ROOT.'/calendar/inc'))
747
			{
748
				$fb_url = true;
749
				$user = isset($data['account_lid']) ? $data['account_lid'] : $GLOBALS['egw']->accounts->id2name($data['account_id']);
750
				$data['freebusy_uri'] = calendar_bo::freebusy_url($user);
751
			}
752
		}
753
		return $data;
754
	}
755
756
	/**
757
	 * src for photo: returns array with linkparams if jpeg exists or the $default image-name if not
758
	 * @param int $id contact_id
759
	 * @param boolean $jpeg =false jpeg exists or not
760
	 * @param string $default ='' image-name to use if !$jpeg, eg. 'template'
761
	 * @param string $etag =null etag to set in url to allow caching with Expires header
762
	 * @return string
763
	 */
764
	function photo_src($id,$jpeg,$default='',$etag=null)
765
	{
766
		//error_log(__METHOD__."($id, ..., etag=$etag) ".  function_backtrace());
767
		return $jpeg || !$default ? Egw::link('/api/avatar.php', array(
768
			'contact_id' => $id,
769
			'lavatar' => !$jpeg ? true : false
770
		)+(isset($etag) ? array(
771
			'etag'       => $etag
772
		) : array())) : $default;
773
	}
774
775
	/**
776
	 * changes the data from your work-format to the db-format
777
	 *
778
	 * It gets called everytime when data gets writen into db or on keys for db-searches
779
	 * this needs to be reimplemented in the derived class
780
	 *
781
	 * @param array $data
782
	 * @param $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
0 ignored issues
show
Documentation Bug introduced by
The doc comment ='ts' at position 0 could not be parsed: Unknown type name '='ts'' at position 0 in ='ts'.
Loading history...
783
	 *
784
	 * @return array upated data
785
	 */
786
	function data2db($data, $date_format='ts')
787
	{
788
		// convert timestamps from user-time to server-time in the db
789
		foreach ($this->timestamps as $name)
790
		{
791
			if (isset($data[$name]))
792
			{
793
				$data[$name] = DateTime::user2server($data[$name], $date_format);
794
			}
795
		}
796
		return $data;
797
	}
798
799
	/**
800
	* deletes contact in db
801
	*
802
	* @param mixed &$contact contact array with key id or (array of) id(s)
803
	* @param boolean $deny_account_delete =true if true never allow to delete accounts
804
	* @param int $check_etag =null
805
	* @return boolean|int true on success or false on failiure, 0 if etag does not match
806
	*/
807
	function delete($contact,$deny_account_delete=true,$check_etag=null)
808
	{
809
		if (is_array($contact) && isset($contact['id']))
810
		{
811
			$contact = array($contact);
812
		}
813
		elseif (!is_array($contact))
814
		{
815
			$contact = array($contact);
816
		}
817
		foreach($contact as $c)
818
		{
819
			$id = is_array($c) ? $c['id'] : $c;
820
821
			$ok = false;
822
			if ($this->check_perms(Acl::DELETE,$c,$deny_account_delete))
823
			{
824
				if (!($old = $this->read($id))) return false;
825
				// check if we only mark contacts as deleted, or really delete them
826
				// already marked as deleted item and accounts are always really deleted
827
				// we cant mark accounts as deleted, as no such thing exists for accounts!
828
				if ($old['owner'] && $this->delete_history != '' && $old['tid'] != self::DELETED_TYPE)
829
				{
830
					$delete = $old;
831
					$delete['tid'] = self::DELETED_TYPE;
832
					if ($check_etag) $delete['etag'] = $check_etag;
833
					if (($ok = $this->save($delete))) $ok = true;	// we have to return true or false
834
					Link::unlink(0,'addressbook',$id,'','','',true);
835
				}
836
				elseif (($ok = parent::delete($id,$check_etag)))
837
				{
838
					Link::unlink(0,'addressbook',$id);
839
				}
840
841
				// Don't notify of final purge
842
				if ($ok && $old['tid'] != self::DELETED_TYPE)
843
				{
844
					if (!isset($this->tracking)) $this->tracking = new Contacts\Tracking($this);
845
					$this->tracking->track(array('id' => $id), array('id' => $id), null, true);
846
				}
847
			}
848
			else
849
			{
850
				break;
851
			}
852
		}
853
		//error_log(__METHOD__.'('.array2string($contact).', deny_account_delete='.array2string($deny_account_delete).', check_etag='.array2string($check_etag).' returning '.array2string($ok));
854
		return $ok;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ok seems to be defined by a foreach iteration on line 817. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
855
	}
856
857
	/**
858
	* saves contact to db
859
	*
860
	* @param array &$contact contact array from etemplate::exec
861
	* @param boolean $ignore_acl =false should the acl be checked or not
862
	* @param boolean $touch_modified =true should modified/r be updated
863
	* @return int/string/boolean id on success, false on failure, the error-message is in $this->error
0 ignored issues
show
Documentation Bug introduced by
The doc comment int/string/boolean at position 0 could not be parsed: Unknown type name 'int/string/boolean' at position 0 in int/string/boolean.
Loading history...
864
	*/
865
	function save(&$contact, $ignore_acl=false, $touch_modified=true)
866
	{
867
		// Make sure photo remains unchanged unless its purposely set to be false
868
		// which means photo has changed.
869
		if (!array_key_exists('photo_unchanged',$contact)) $contact['photo_unchanged'] = true;
870
871
		// remember if we add or update a entry
872
		if (($isUpdate = $contact['id']))
873
		{
874
			if (!isset($contact['owner']) || !isset($contact['private']))	// owner/private not set on update, eg. SyncML
875
			{
876
				if (($old = $this->read($contact['id'])))	// --> try reading the old entry and set it from there
877
				{
878
					if(!isset($contact['owner']))
879
					{
880
						$contact['owner'] = $old['owner'];
881
					}
882
					if(!isset($contact['private']))
883
					{
884
						$contact['private'] = $old['private'];
885
					}
886
				}
887
				else	// entry not found --> create a new one
888
				{
889
					$isUpdate = $contact['id'] = null;
890
				}
891
			}
892
		}
893
		else
894
		{
895
			// if no owner/addressbook set use the setting of the add_default prefs (if set, otherwise the users personal addressbook)
896
			if (!isset($contact['owner'])) $contact['owner'] = $this->default_addressbook;
897
			if (!isset($contact['private'])) $contact['private'] = (int)$this->default_private;
898
			// do NOT allow to create new accounts via addressbook, they are broken without an account_id
899
			if (!$contact['owner'] && empty($contact['account_id']))
900
			{
901
				$contact['owner'] = $this->default_addressbook ? $this->default_addressbook : $this->user;
902
			}
903
			// allow admins to import contacts with creator / created date set
904
			if (!$contact['creator'] || !$ignore_acl && !$this->is_admin($contact)) $contact['creator'] = $this->user;
905
			if (!$contact['created'] || !$ignore_acl && !$this->is_admin($contact)) $contact['created'] = $this->now_su;
906
907
			if (!$contact['tid']) $contact['tid'] = 'n';
908
		}
909
		// ensure accounts and group addressbooks are never private!
910
		if ($contact['owner'] <= 0)
911
		{
912
			$contact['private'] = 0;
913
		}
914
		if(!$ignore_acl && !$this->check_perms($isUpdate ? Acl::EDIT : Acl::ADD,$contact))
915
		{
916
			$this->error = 'access denied';
917
			return false;
918
		}
919
		// resize image to 60px width
920
		if (!empty($contact['jpegphoto']))
921
		{
922
			$contact['jpegphoto'] = $this->resize_photo($contact['jpegphoto']);
923
		}
924
		// convert categories
925
		if (is_array($contact['cat_id']))
926
		{
927
			$contact['cat_id'] = implode(',',$contact['cat_id']);
928
		}
929
930
		// Update country codes
931
		foreach(array('adr_one_', 'adr_two_') as $c_prefix) {
932
			if($contact[$c_prefix.'countryname'] && !$contact[$c_prefix.'countrycode'] &&
933
				$code = Country::country_code($contact[$c_prefix.'countryname']))
934
			{
935
				if(strlen($code) == 2)
936
				{
937
					$contact[$c_prefix.'countrycode'] = $code;
938
				}
939
				else
940
				{
941
					$contact[$c_prefix.'countrycode'] = null;
942
				}
943
			}
944
			if($contact[$c_prefix.'countrycode'] != null)
945
			{
946
				$contact[$c_prefix.'countryname'] = null;
947
			}
948
		}
949
950
		// last modified
951
		if ($touch_modified)
952
		{
953
			$contact['modifier'] = $this->user;
954
			$contact['modified'] = $this->now_su;
955
		}
956
		// set full name and fileas from the content
957
		if (!isset($contact['n_fn']))
958
		{
959
			$contact['n_fn'] = $this->fullname($contact);
960
		}
961
		if (isset($contact['org_name'])) $contact['n_fileas'] = $this->fileas($contact, null, false);
962
963
		// Get old record for tracking changes
964
		if (!isset($old) && $isUpdate)
965
		{
966
			$old = $this->read($contact['id']);
967
		}
968
		$to_write = $contact;
969
		// (non-admin) user editing his own account, make sure he does not change fields he is not allowed to (eg. via SyncML or xmlrpc)
970
		if (!$ignore_acl && !$contact['owner'] && !($this->is_admin($contact) || $this->allow_account_edit()))
971
		{
972
			foreach(array_keys($contact) as $field)
973
			{
974
				if (!in_array($field,$this->own_account_acl) && !in_array($field,array('id','owner','account_id','modified','modifier', 'photo_unchanged')))
975
				{
976
					// user is not allowed to change that
977
					if ($old)
978
					{
979
						$to_write[$field] = $contact[$field] = $old[$field];
980
					}
981
					else
982
					{
983
						unset($to_write[$field]);
984
					}
985
				}
986
			}
987
		}
988
989
		// IF THE OLD ENTRY IS A ACCOUNT, dont allow to change the owner/location
990
		// maybe we need that for id and account_id as well.
991
		if (is_array($old) && (!isset($old['owner']) || empty($old['owner'])))
992
		{
993
			if (isset($to_write['owner']) && !empty($to_write['owner']))
994
			{
995
				error_log(__METHOD__.__LINE__." Trying to change account to owner:". $to_write['owner'].' Account affected:'.array2string($old).' Data send:'.array2string($to_write));
996
				unset($to_write['owner']);
997
			}
998
		}
999
1000
		if(!($this->error = parent::save($to_write)))
1001
		{
1002
			$contact['id'] = $to_write['id'];
1003
			$contact['uid'] = $to_write['uid'];
1004
			$contact['etag'] = $to_write['etag'];
1005
			$contact['files'] = $to_write['files'];
1006
1007
			// Clear any files saved with new entries
1008
			// They've been dealt with already and they cause errors with linking
1009
			foreach(array_keys($this->customfields) as $field)
1010
			{
1011
				if(is_array($to_write[Storage::CF_PREFIX.$field]))
1012
				{
1013
					unset($to_write[Storage::CF_PREFIX.$field]);
1014
				}
1015
			}
1016
1017
			// if contact is an account and account-relevant data got updated, handle it like account got updated
1018
			if ($contact['account_id'] && $isUpdate &&
1019
				($old['email'] != $contact['email'] || $old['n_family'] != $contact['n_family'] || $old['n_given'] != $contact['n_given']))
1020
			{
1021
				// invalidate the cache of the accounts class
1022
				$GLOBALS['egw']->accounts->cache_invalidate($contact['account_id']);
1023
				// call edit-accout hook, to let other apps know about changed account (names or email)
1024
				$GLOBALS['hook_values'] = (array)$GLOBALS['egw']->accounts->read($contact['account_id']);
1025
				Hooks::process($GLOBALS['hook_values']+array(
1026
					'location' => 'editaccount',
1027
				),False,True);	// called for every app now, not only enabled ones)
1028
			}
1029
			// notify interested apps about changes in the account-contact data
1030
			if (!$to_write['owner'] && $to_write['account_id'] && $isUpdate)
1031
			{
1032
				$to_write['location'] = 'editaccountcontact';
1033
				Hooks::process($to_write,False,True);	// called for every app now, not only enabled ones));
1034
			}
1035
			// Notify linked apps about changes in the contact data
1036
			Link::notify_update('addressbook',  $contact['id'], $contact);
1037
1038
			// Check for restore of deleted contact, restore held links
1039
			if($old && $old['tid'] == self::DELETED_TYPE && $contact['tid'] != self::DELETED_TYPE)
1040
			{
1041
				Link::restore('addressbook', $contact['id']);
1042
			}
1043
1044
			// Record change history for sql - doesn't work for LDAP accounts
1045
			if(!$contact['account_id'] || $contact['account_id'] && $this->account_repository == 'sql')
1046
			{
1047
				$deleted = ($old['tid'] == self::DELETED_TYPE || $contact['tid'] == self::DELETED_TYPE);
1048
				if (!isset($this->tracking)) $this->tracking = new Contacts\Tracking($this);
1049
				$this->tracking->track($to_write, $old ? $old : null, null, $deleted);
1050
			}
1051
1052
			// Expire birthday cache for this year and next if birthday changed
1053
			if($isUpdate && $old['bday'] !== $to_write['bday'] || !$isUpdate && $to_write['bday'])
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($isUpdate && $old['bday...te && $to_write['bday'], Probably Intended Meaning: $isUpdate && ($old['bday...e && $to_write['bday'])
Loading history...
1054
			{
1055
				$year = (int) date('Y',time());
1056
				$this->clear_birthday_cache($year, $to_write['owner']);
1057
				$year++;
1058
				$this->clear_birthday_cache($year, $to_write['owner']);
1059
			}
1060
		}
1061
1062
		return $this->error ? false : $contact['id'];
1063
	}
1064
1065
	/**
1066
	 * Since birthdays are cached for the instance for BIRTHDAY_CACHE_TIME, we
1067
	 * need to clear them if a birthday changes.
1068
	 *
1069
	 * @param type $year
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1070
	 */
1071
	protected function clear_birthday_cache($year, $owner)
1072
	{
1073
		// Cache is kept per-language, so clear them all
1074
		foreach(array_keys(Translation::get_installed_langs()) as $lang)
1075
		{
1076
			Cache::unsetInstance(__CLASS__,"birthday-$year-{$owner}-$lang");
1077
		}
1078
	}
1079
1080
	/**
1081
	 * Resize photo down to 240pixel width and returns it
1082
	 *
1083
	 * Also makes sures photo is a JPEG.
1084
	 *
1085
	 * @param string|FILE $photo string with image or open filedescribtor
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\FILE was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1086
	 * @param int $dst_w =240 max width to resize to
1087
	 * @return string with resized jpeg photo, null on error
1088
	 */
1089
	public static function resize_photo($photo, $dst_w=240)
1090
	{
1091
		if (is_resource($photo))
0 ignored issues
show
introduced by
The condition is_resource($photo) is always false.
Loading history...
1092
		{
1093
			$photo = stream_get_contents($photo);
1094
		}
1095
		if (empty($photo) || !($image = imagecreatefromstring($photo)))
1096
		{
1097
			error_log(__METHOD__."() invalid image!");
1098
			return null;
1099
		}
1100
		$src_w = imagesx($image);
1101
		$src_h = imagesy($image);
1102
		//error_log(__METHOD__."() got image $src_w * $src_h, is_jpeg=".array2string(substr($photo,0,2) === "\377\330"));
1103
1104
		// if $photo is to width or not a jpeg image --> resize it
1105
		if ($src_w > $dst_w || cut_bytes($photo,0,2) !== "\377\330")
1106
		{
1107
			//error_log(__METHOD__."(,dst_w=$dst_w) src_w=$src_w, cut_bytes(photo,0,2)=".array2string(cut_bytes($photo,0,2)).' --> resizing');
1108
			// scale the image to a width of 60 and a height according to the proportion of the source image
1109
			$resized = imagecreatetruecolor($dst_w,$dst_h = round($src_h * $dst_w / $src_w));
1110
			imagecopyresized($resized,$image,0,0,0,0,$dst_w,$dst_h,$src_w,$src_h);
1111
1112
			ob_start();
1113
			imagejpeg($resized,null,90);
1114
			$photo = ob_get_contents();
1115
			ob_end_clean();
1116
1117
			imagedestroy($resized);
1118
			//error_log(__METHOD__."() resized image $src_w*$src_h to $dst_w*$dst_h");
1119
		}
1120
		//else error_log(__METHOD__."(,dst_w=$dst_w) src_w=$src_w, cut_bytes(photo,0,2)=".array2string(cut_bytes($photo,0,2)).' --> NOT resizing');
1121
1122
		imagedestroy($image);
1123
1124
		return $photo;
1125
	}
1126
1127
	/**
1128
	* reads contacts matched by key and puts all cols in the data array
1129
	*
1130
	* @param int|string $contact_id
1131
	* @param boolean $ignore_acl =false true: no acl check
1132
	* @return array|boolean array with contact data, null if not found or false on no view perms
1133
	*/
1134
	function read($contact_id, $ignore_acl=false)
1135
	{
1136
		// get so_sql_cf to read private customfields too, if we ignore acl
1137
		if ($ignore_acl && is_a($this->somain, __CLASS__.'\\Sql'))
1138
		{
1139
			$cf_backup = (array)$this->somain->customfields;
1140
			$this->somain->customfields = Storage\Customfields::get('addressbook', true);
1141
		}
1142
		if (!($data = parent::read($contact_id)))
1143
		{
1144
			$data = null;	// not found
1145
		}
1146
		elseif (!$ignore_acl && !$this->check_perms(Acl::READ,$data))
1147
		{
1148
			$data = false;	// no view perms
1149
		}
1150
		else
1151
		{
1152
			// determine the file-as type
1153
			$data['fileas_type'] = $this->fileas_type($data);
1154
1155
			// Update country name from code
1156
			if($data['adr_one_countrycode'] != null) {
1157
				$data['adr_one_countryname'] = Country::get_full_name($data['adr_one_countrycode'], true);
1158
			}
1159
			if($data['adr_two_countrycode'] != null) {
1160
				$data['adr_two_countryname'] = Country::get_full_name($data['adr_two_countrycode'], true);
1161
			}
1162
		}
1163
		if (isset($cf_backup))
1164
		{
1165
			$this->somain->customfields = $cf_backup;
1166
		}
1167
		//error_log(__METHOD__.'('.array2string($contact_id).') returning '.array2string($data));
1168
		return $data;
1169
	}
1170
1171
	/**
1172
	 * Checks if the current user has the necessary ACL rights
1173
	 *
1174
	 * If the access of a contact is set to private, one need a private grant for a personal addressbook
1175
	 * or the group membership for a group-addressbook
1176
	 *
1177
	 * @param int $needed necessary ACL right: Acl::{READ|EDIT|DELETE}
1178
	 * @param mixed $contact contact as array or the contact-id
1179
	 * @param boolean $deny_account_delete =false if true never allow to delete accounts
1180
	 * @param int $user =null for which user to check, default current user
1181
	 * @return boolean true permission granted, false for permission denied, null for contact does not exist
1182
	 */
1183
	function check_perms($needed,$contact,$deny_account_delete=false,$user=null)
1184
	{
1185
		if (!$user) $user = $this->user;
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1186
		if ($user == $this->user)
1187
		{
1188
			$grants = $this->grants;
1189
			$memberships = $this->memberships;
1190
		}
1191
		else
1192
		{
1193
			$grants = $this->get_grants($user);
1194
			$memberships =  $GLOBALS['egw']->accounts->memberships($user,true);
1195
		}
1196
1197
		if ((!is_array($contact) || !isset($contact['owner'])) &&
1198
1199
			!($contact = parent::read(is_array($contact) ? $contact['id'] : $contact)))
1200
		{
1201
			return null;
1202
		}
1203
		$owner = $contact['owner'];
1204
1205
		// allow the user to edit his own account
1206
		if (!$owner && $needed == Acl::EDIT && $contact['account_id'] == $user && $this->own_account_acl)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->own_account_acl 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...
1207
		{
1208
			$access = true;
1209
		}
1210
		// dont allow to delete own account (as admin handels it too)
1211
		elseif (!$owner && $needed == Acl::DELETE && ($deny_account_delete || $contact['account_id'] == $user))
1212
		{
1213
			$access = false;
1214
		}
1215
		// for reading accounts (owner == 0) and account_selection == groupmembers, check if current user and contact are groupmembers
1216
		elseif ($owner == 0 && $needed == Acl::READ &&
1217
			$GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'groupmembers' &&
1218
			!isset($GLOBALS['egw_info']['user']['apps']['admin']))
1219
		{
1220
			$access = !!array_intersect($memberships,$GLOBALS['egw']->accounts->memberships($contact['account_id'],true));
1221
		}
1222
		else if ($contact['id'] && $GLOBALS['egw']->acl->check('A'.$contact['id'], $needed, 'addressbook'))
1223
		{
1224
			$access = true;
1225
		}
1226
		else
1227
		{
1228
			$access = ($grants[$owner] & $needed) &&
1229
				(!$contact['private'] || ($grants[$owner] & Acl::PRIVAT) || in_array($owner,$memberships));
1230
		}
1231
		//error_log(__METHOD__."($needed,$contact[id],$deny_account_delete,$user) returning ".array2string($access));
1232
		return $access;
1233
	}
1234
1235
	/**
1236
	 * Check access to the file store
1237
	 *
1238
	 * @param int|array $id id of entry or entry array
1239
	 * @param int $check Acl::READ for read and Acl::EDIT for write or delete access
1240
	 * @param string $rel_path =null currently not used in InfoLog
1241
	 * @param int $user =null for which user to check, default current user
1242
	 * @return boolean true if access is granted or false otherwise
1243
	 */
1244
	function file_access($id,$check,$rel_path=null,$user=null)
1245
	{
1246
		unset($rel_path);	// not used, but required by function signature
1247
1248
		return $this->check_perms($check,$id,false,$user);
1249
	}
1250
1251
	/**
1252
	 * Read (virtual) org-entry (values "common" for most contacts in the given org)
1253
	 *
1254
	 * @param string $org_id org_name:oooooo|||org_unit:uuuuuuuuu|||adr_one_locality:lllllll (org_unit and adr_one_locality are optional)
1255
	 * @return array/boolean array with common org fields or false if org not found
0 ignored issues
show
Documentation Bug introduced by
The doc comment array/boolean at position 0 could not be parsed: Unknown type name 'array/boolean' at position 0 in array/boolean.
Loading history...
1256
	 */
1257
	function read_org($org_id)
1258
	{
1259
		if (!$org_id) return false;
1260
		if (strpos($org_id,'*AND*')!== false) $org_id = str_replace('*AND*','&',$org_id);
1261
		$org = array();
1262
		foreach(explode('|||',$org_id) as $part)
1263
		{
1264
			list($name,$value) = explode(':',$part,2);
1265
			$org[$name] = $value;
1266
		}
1267
		$csvs = array('cat_id');	// fields with comma-separated-values
1268
1269
		// split regular fields and custom fields
1270
		$custom_fields = $regular_fields = array();
1271
		foreach($this->org_fields as $name)
1272
		{
1273
			if ($name[0] != '#')
1274
			{
1275
				$regular_fields[] = $name;
1276
			}
1277
			else
1278
			{
1279
				$custom_fields[] = $name = substr($name,1);
1280
				$regular_fields['id'] = 'id';
1281
				if (substr($this->customfields[$name]['type'],0,6)=='select' && $this->customfields[$name]['rows'] ||	// multiselection
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (substr($this->customfie...ame]['type'] == 'radio', Probably Intended Meaning: substr($this->customfiel...me]['type'] == 'radio')
Loading history...
1282
					$this->customfields[$name]['type'] == 'radio')
1283
				{
1284
					$csvs[] = '#'.$name;
1285
				}
1286
			}
1287
		}
1288
		// read the regular fields
1289
		$contacts = parent::search('',$regular_fields,'','','',false,'AND',false,$org);
1290
		if (!$contacts) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $contacts 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...
1291
1292
		// if we have custom fields, read and merge them in
1293
		if ($custom_fields)
1294
		{
1295
			foreach($contacts as $contact)
1296
			{
1297
				$ids[] = $contact['id'];
1298
			}
1299
			if (($cfs = $this->read_customfields($ids,$custom_fields)))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ids seems to be defined by a foreach iteration on line 1295. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1300
			{
1301
				foreach ($contacts as &$contact)
1302
				{
1303
					$id = $contact['id'];
1304
					if (isset($cfs[$id]))
1305
					{
1306
						foreach($cfs[$id] as $name => $value)
1307
						{
1308
							$contact['#'.$name] = $value;
1309
						}
1310
					}
1311
				}
1312
				unset($contact);
1313
			}
1314
		}
1315
1316
		// create a statistic about the commonness of each fields values
1317
		$fields = array();
1318
		foreach($contacts as $contact)
1319
		{
1320
			foreach($contact as $name => $value)
1321
			{
1322
				if (!in_array($name,$csvs))
1323
				{
1324
					$fields[$name][$value]++;
1325
				}
1326
				else
1327
				{
1328
					// for comma separated fields, we have to use each single value
1329
					foreach(explode(',',$value) as $val)
1330
					{
1331
						$fields[$name][$val]++;
1332
					}
1333
				}
1334
			}
1335
		}
1336
		foreach($fields as $name => $values)
1337
		{
1338
			if (!in_array($name,$this->org_fields)) continue;
1339
1340
			arsort($values,SORT_NUMERIC);
1341
			$value = key($values);
1342
			$num = current($values);
1343
			if ($value && $num / (double) count($contacts) >= $this->org_common_factor)
1344
			{
1345
				if (!in_array($name,$csvs))
1346
				{
1347
					$org[$name] = $value;
1348
				}
1349
				else
1350
				{
1351
					$org[$name] = array();
1352
					foreach ($values as $value => $num)
1353
					{
1354
						if ($value && $num / (double) count($contacts) >= $this->org_common_factor)
1355
						{
1356
							$org[$name][] = $value;
1357
						}
1358
					}
1359
					$org[$name] = implode(',',$org[$name]);
1360
				}
1361
			}
1362
		}
1363
		return $org;
1364
	}
1365
1366
	/**
1367
	 * Return all org-members with same content in one or more of the given fields (only org_fields are counting)
1368
	 *
1369
	 * @param string $org_name
1370
	 * @param array $fields field-name => value pairs
1371
	 * @return array with contacts
1372
	 */
1373
	function org_similar($org_name,$fields)
1374
	{
1375
		$criteria = array();
1376
		foreach($this->org_fields as $name)
1377
		{
1378
			if (isset($fields[$name]))
1379
			{
1380
				if (empty($fields[$name]))
1381
				{
1382
					$criteria[] = "($name IS NULL OR $name='')";
1383
				}
1384
				else
1385
				{
1386
					$criteria[$name] = $fields[$name];
1387
				}
1388
			}
1389
		}
1390
		return parent::search($criteria,false,'n_family,n_given','','',false,'OR',false,array('org_name'=>$org_name));
1391
	}
1392
1393
	/**
1394
	 * Return the changed fields from two versions of a contact (not modified or modifier)
1395
	 *
1396
	 * @param array $from original/old version of the contact
1397
	 * @param array $to changed/new version of the contact
1398
	 * @param boolean $only_org_fields =true check and return only org_fields, default true
1399
	 * @return array with field-name => value from $from
1400
	 */
1401
	function changed_fields($from,$to,$only_org_fields=true)
1402
	{
1403
		// we only care about countryname, if contrycode is empty
1404
		foreach(array(
1405
			'adr_one_countryname' => 'adr_one_countrycode',
1406
			'adr_two_countryname' => 'adr_one_countrycode',
1407
		) as $name => $code)
1408
		{
1409
			if (!empty($from[$code])) $from[$name] = '';
1410
			if (!empty($to[$code])) $to[$name] = '';
1411
		}
1412
		$changed = array();
1413
		foreach($only_org_fields ? $this->org_fields : array_keys($this->contact_fields) as $name)
1414
		{
1415
			if (in_array($name,array('modified','modifier')))	// never count these
1416
			{
1417
				continue;
1418
			}
1419
			if ((string) $from[$name] != (string) $to[$name])
1420
			{
1421
				$changed[$name] = $from[$name];
1422
			}
1423
		}
1424
		return $changed;
1425
	}
1426
1427
	/**
1428
	 * Change given fields in all members of the org with identical content in the field
1429
	 *
1430
	 * @param string $org_name
1431
	 * @param array $from original/old version of the contact
1432
	 * @param array $to changed/new version of the contact
1433
	 * @param array $members =null org-members to change, default null --> function queries them itself
1434
	 * @return array/boolean (changed-members,changed-fields,failed-members) or false if no org_fields changed or no (other) members matching that fields
0 ignored issues
show
Documentation Bug introduced by
The doc comment array/boolean at position 0 could not be parsed: Unknown type name 'array/boolean' at position 0 in array/boolean.
Loading history...
1435
	 */
1436
	function change_org($org_name,$from,$to,$members=null)
1437
	{
1438
		if (!($changed = $this->changed_fields($from,$to,true))) return false;
1439
1440
		if (is_null($members) || !is_array($members))
1441
		{
1442
			$members = $this->org_similar($org_name,$changed);
1443
		}
1444
		if (!$members) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $members 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...
1445
1446
		$ids = array();
1447
		foreach($members as $member)
1448
		{
1449
			$ids[] = $member['id'];
1450
		}
1451
		$customfields = $this->read_customfields($ids);
1452
1453
		$changed_members = $changed_fields = $failed_members = 0;
1454
		foreach($members as $member)
1455
		{
1456
			if (isset($customfields[$member['id']]))
1457
			{
1458
				foreach(array_keys($this->customfields) as $name)
1459
				{
1460
					$member['#'.$name] = $customfields[$member['id']][$name];
1461
				}
1462
			}
1463
			$fields = 0;
1464
			foreach($changed as $name => $value)
1465
			{
1466
				if ((string)$value == (string)$member[$name])
1467
				{
1468
					$member[$name] = $to[$name];
1469
					++$fields;
1470
				}
1471
			}
1472
			if ($fields)
1473
			{
1474
				if (!$this->check_perms(Acl::EDIT,$member) || !$this->save($member))
1475
				{
1476
					++$failed_members;
1477
				}
1478
				else
1479
				{
1480
					++$changed_members;
1481
					$changed_fields += $fields;
1482
				}
1483
			}
1484
		}
1485
		return array($changed_members,$changed_fields,$failed_members);
1486
	}
1487
1488
	/**
1489
	 * get title for a contact identified by $contact
1490
	 *
1491
	 * Is called as hook to participate in the linking. The format is determined by the link_title preference.
1492
	 *
1493
	 * @param int|string|array $contact int/string id or array with contact
1494
	 * @return string/boolean string with the title, null if contact does not exitst, false if no perms to view it
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/boolean at position 0 could not be parsed: Unknown type name 'string/boolean' at position 0 in string/boolean.
Loading history...
1495
	 */
1496
	function link_title($contact)
1497
	{
1498
		if (!is_array($contact) && $contact)
1499
		{
1500
			$contact = $this->read($contact);
1501
		}
1502
		if (!is_array($contact))
1503
		{
1504
			return $contact;
1505
		}
1506
		$type = $this->prefs['link_title'];
1507
		if (!$type || $type === 'n_fileas')
1508
		{
1509
			if ($contact['n_fileas']) return $contact['n_fileas'];
1510
			$type = null;
1511
		}
1512
		$title =  $this->fileas($contact,$type);
1513
1514
		if (!empty($this->prefs['link_title_cf']))
1515
		{
1516
			$field_list = is_string($this->prefs['link_title_cf']) ? explode(',', $this->prefs['link_title_cf']) : $this->prefs['link_title_cf'];
1517
			foreach ($field_list as $field)
1518
			{
1519
				if($contact['#'.$field])
1520
				{
1521
				   $title .= ', ' . $contact['#'.$field];
1522
				}
1523
			}
1524
		}
1525
		return $title ;
1526
	}
1527
1528
	/**
1529
	 * get title for multiple contacts identified by $ids
1530
	 *
1531
	 * Is called as hook to participate in the linking. The format is determined by the link_title preference.
1532
	 *
1533
	 * @param array $ids array with contact-id's
1534
	 * @return array with titles, see link_title
1535
	 */
1536
	function link_titles(array $ids)
1537
	{
1538
		$titles = array();
1539
		if (($contacts =& $this->search(array('contact_id' => $ids),false,'',$extra_cols='','',False,'AND',False,array('tid'=>null))))
1540
		{
1541
			$ids = array();
1542
			foreach($contacts as $contact)
1543
			{
1544
				$ids[] = $contact['id'];
1545
			}
1546
			$cfs = $this->read_customfields($ids);
1547
			foreach($contacts as $contact)
1548
			{
1549
			   	$titles[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]);
1550
			}
1551
		}
1552
		// we assume all not returned contacts are not readable for the user (as we report all deleted contacts to egw_link)
1553
		foreach($ids as $id)
1554
		{
1555
			if (!isset($titles[$id]))
1556
			{
1557
				$titles[$id] = false;
1558
			}
1559
		}
1560
		return $titles;
1561
	}
1562
1563
	/**
1564
	 * query addressbook for contacts matching $pattern
1565
	 *
1566
	 * Is called as hook to participate in the linking
1567
	 *
1568
	 * @param string|array $pattern pattern to search, or an array with a 'search' key
1569
	 * @param array $options Array of options for the search
1570
	 * @return array with id - title pairs of the matching entries
1571
	 */
1572
	function link_query($pattern, Array &$options = array())
1573
	{
1574
		$result = $criteria = array();
1575
		$limit = false;
1576
		if ($pattern)
1577
		{
1578
			$criteria = is_array($pattern) ? $pattern['search'] : $pattern;
1579
		}
1580
		if($options['start'] || $options['num_rows'])
1581
		{
1582
			$limit = array($options['start'], $options['num_rows']);
1583
		}
1584
		$filter = (array)$options['filter'];
1585
		if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '1') $filter['account_id'] = null;
1586
		if (($contacts =& parent::search($criteria,false,'org_name,n_family,n_given,cat_id,contact_email','','%',false,'OR', $limit, $filter)))
1587
		{
1588
			$ids = array();
1589
			foreach($contacts as $contact)
1590
			{
1591
				$ids[] = $contact['id'];
1592
			}
1593
			$cfs = $this->read_customfields($ids);
1594
			foreach($contacts as $contact)
1595
			{
1596
				$result[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]);
1597
				// make sure to return a correctly quoted rfc822 address, if requested
1598
				if ($options['type'] === 'email')
1599
				{
1600
					$args = explode('@', $contact['email']);
1601
					$args[] = $result[$contact['id']];
1602
					$result[$contact['id']] = call_user_func_array('imap_rfc822_write_address', $args);
1603
				}
1604
				// show category color
1605
				if ($contact['cat_id'] && ($color = Categories::cats2color($contact['cat_id'])))
1606
				{
1607
					$result[$contact['id']] = array(
1608
						'label' => $result[$contact['id']],
1609
						'style.backgroundColor' => $color,
1610
					);
1611
				}
1612
			}
1613
		}
1614
		$options['total'] = $this->total;
1615
		return $result;
1616
	}
1617
1618
	/**
1619
	 * Query for subtype email (returns only contacts with email address set)
1620
	 *
1621
	 * @param string|array $pattern
1622
	 * @param array $options
1623
	 * @return Ambigous <multitype:, string, multitype:Ambigous <multitype:, string> string >
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Ambigous was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1624
	 */
1625
	function link_query_email($pattern, Array &$options = array())
1626
	{
1627
		if (isset($options['filter']) && !is_array($options['filter']))
1628
		{
1629
			$options['filter'] = (array)$options['filter'];
1630
		}
1631
		// return only contacts with email set
1632
		$options['filter'][] = "contact_email ".$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE]." '%@%'";
1633
1634
		// let link query know, to append email to list
1635
		$options['type'] = 'email';
1636
1637
		return $this->link_query($pattern,$options);
1638
	}
1639
1640
	/**
1641
	 * returns info about contacts for calender
1642
	 *
1643
	 * @param int|array $ids single contact-id or array of id's
1644
	 * @return array
1645
	 */
1646
	function calendar_info($ids)
1647
	{
1648
		if (!$ids) return null;
1649
1650
		$data = array();
1651
		foreach(!is_array($ids) ? array($ids) : $ids as $id)
1652
		{
1653
			if (!($contact = $this->read($id))) continue;
1654
1655
			$data[] = array(
1656
				'res_id' => $id,
1657
				'email' => $contact['email'] ? $contact['email'] : $contact['email_home'],
1658
				'rights' => Acl::CUSTOM1|Acl::CUSTOM3,	// calendar_bo::ACL_READ_FOR_PARTICIPANTS|ACL_INVITE
1659
				'name' => $this->link_title($contact),
1660
				'cn' => trim($contact['n_given'].' '.$contact['n_family']),
1661
			);
1662
		}
1663
		return $data;
1664
	}
1665
1666
	/**
1667
	 * Read the next and last event of given contacts
1668
	 *
1669
	 * @param array $uids participant IDs.  Contacts should be c<contact_id>, user accounts <account_id>
1670
	 * @param boolean $extra_title =true if true, use a short date only title and put the full title as extra_title (tooltip)
1671
	 * @return array
1672
	 */
1673
	function read_calendar($uids,$extra_title=true)
1674
	{
1675
		if (!$GLOBALS['egw_info']['user']['apps']['calendar']) return array();
1676
1677
		$split_uids = array();
1678
		$events = array();
1679
1680
		foreach($uids as $id => $uid)
1681
		{
1682
			$type = is_numeric($uid[0]) ? 'u' : $uid[0];
1683
			if($GLOBALS['egw_info']['server']['disable_event_column'] == 'contacts' && $type == 'u')
1684
			{
1685
				continue;
1686
			}
1687
			$split_uids[$type][$id] = str_replace($type, '', $uid);
1688
		}
1689
1690
		foreach($split_uids as $type => $s_uids)
1691
		{
1692
			$events += $this->read_calendar_type($s_uids, $type, $extra_title);
1693
		}
1694
		return $events;
1695
	}
1696
1697
	private function read_calendar_type($uids, $type='c', $extra_title = true)
1698
	{
1699
		$calendars = array();
1700
		$bocal = new calendar_bo();
1701
		$type_field = $type=='u' ? 'account_id' : 'contact_id';
1702
		$type_field_varchar = $this->db->to_varchar($type_field);
1703
		$concat_start_id_recurrance = $this->db->concat('cal_start',"':'",'egw_cal_user.cal_id',"':'",'cal_recur_date');
1704
		$now = $this->db->unix_timestamp('NOW()');
1705
		$sql = "SELECT n_fn,org_name,$type_field AS user_id,
1706
			(
1707
				SELECT $concat_start_id_recurrance
1708
				FROM egw_cal_user
1709
				JOIN egw_cal_dates on egw_cal_dates.cal_id=egw_cal_user.cal_id and (cal_recur_date=0 or cal_recur_date=cal_start)
1710
				JOIN egw_cal ON egw_cal.cal_id=egw_cal_user.cal_id AND egw_cal.cal_deleted IS NULL
1711
				WHERE cal_user_type='$type' and cal_user_id=$type_field_varchar and cal_start < $now";
1712
		if ( !$GLOBALS['egw_info']['user']['preferences']['calendar']['show_rejected'])
1713
		{
1714
			$sql .= " AND egw_cal_user.cal_status != 'R'";
1715
		}
1716
		$sql .= "
1717
				order by cal_start DESC Limit 1
1718
			) as last_event,
1719
			(
1720
				SELECT $concat_start_id_recurrance
1721
				FROM egw_cal_user
1722
				JOIN egw_cal_dates on egw_cal_dates.cal_id=egw_cal_user.cal_id and (cal_recur_date=0 or cal_recur_date=cal_start)
1723
				JOIN egw_cal ON egw_cal.cal_id=egw_cal_user.cal_id AND egw_cal.cal_deleted IS NULL
1724
				WHERE cal_user_type='$type' and cal_user_id=$type_field_varchar and cal_start > $now";
1725
		if ( !$GLOBALS['egw_info']['user']['preferences']['calendar']['show_rejected'])
1726
		{
1727
			$sql .= " AND egw_cal_user.cal_status != 'R'";
1728
		}
1729
		$sql .= ' order by cal_recur_date ASC, cal_start ASC Limit 1
1730
1731
			) as next_event
1732
			FROM egw_addressbook
1733
			WHERE '.$this->db->expression('egw_addressbook', array($type_field => $uids));
1734
1735
1736
		$contacts =& $this->db->query($sql, __LINE__, __FILE__);
1737
1738
		if (!$contacts) return array();
1739
1740
		// Extract the event info and generate what is needed for next/last event
1741
		$do_event = function($key, $contact) use (&$bocal, &$calendars, $type, $extra_title)
1742
		{
1743
			list($start, $cal_id, $recur_date) = explode(':', $contact[$key.'_event']);
1744
1745
			$link = array(
1746
				'id' => $cal_id,//.':'.$start,
1747
				'app' => 'calendar',
1748
				'title' => $bocal->link_title($cal_id . ($start ? '-'.$start : '')),
1749
				'extra_args' => array(
1750
					'date' => \EGroupware\Api\DateTime::server2user($start,\EGroupware\Api\DateTime::ET2),
1751
					'exception'=> 1
1752
				),
1753
			);
1754
			if ($extra_title)
1755
			{
1756
				$link['extra_title'] = $link['title'];
1757
				$link['title'] = \EGroupware\Api\DateTime::server2user($start, true);
1758
			}
1759
			$user_id = ($type == 'u' ? '' : $type) . $contact['user_id'];
1760
			$calendars[$user_id][$key.'_event'] = $start;
1761
			$calendars[$user_id][$key.'_link'] = $link;
1762
		};
1763
1764
		foreach($contacts as $contact)
1765
		{
1766
			if($contact['last_event'])
1767
			{
1768
				$do_event('last', $contact);
1769
			}
1770
			if($contact['next_event'])
1771
			{
1772
				$do_event('next', $contact);
1773
			}
1774
		}
1775
		return $calendars;
1776
	}
1777
1778
	/**
1779
	 * Read the holidays (birthdays) from the given addressbook, either from the
1780
	 * instance cache, or read them & cache for next time.  Cached for HOLIDAY_CACHE_TIME.
1781
	 *
1782
	 * @param int $addressbook - Addressbook to search.  We cache them separately in the instance.
1783
	 * @param int $year
1784
	 */
1785
	public function read_birthdays($addressbook, $year)
1786
	{
1787
		if (($birthdays = Cache::getInstance(__CLASS__,"birthday-$year-$addressbook-".$GLOBALS['egw_info']['user']['preferences']['common']['lang'])) !== null)
1788
		{
1789
			return $birthdays;
1790
		}
1791
1792
		$birthdays = array();
1793
		$filter = array(
1794
			'owner' => (int)$addressbook,
1795
			'n_family' => "!''",
1796
			'bday' => "!''",
1797
		);
1798
		$bdays =& $this->search('',array('id','n_family','n_given','n_prefix','n_middle','bday'),
1799
			'contact_bday ASC', '', '', false, 'AND', false, $filter);
1800
1801
		if ($bdays)
0 ignored issues
show
Bug Best Practice introduced by
The expression $bdays 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...
1802
		{
1803
			// sort by month and day only
1804
			usort($bdays, function($a, $b)
1805
			{
1806
				return (int) $a['bday'] == (int) $b['bday'] ?
1807
					strcmp($a['bday'], $b['bday']) :
1808
					(int) $a['bday'] - (int) $b['bday'];
1809
			});
1810
			foreach($bdays as $pers)
1811
			{
1812
				if (empty($pers['bday']) || $pers['bday']=='0000-00-00 0' || $pers['bday']=='0000-00-00' || $pers['bday']=='0.0.00')
1813
				{
1814
					//error_log(__METHOD__.__LINE__.' Skipping entry for invalid birthday:'.array2string($pers));
1815
					continue;
1816
				}
1817
				list($y,$m,$d) = explode('-',$pers['bday']);
1818
				if ($y > $year)
1819
				{
1820
					// not yet born
1821
					continue;
1822
				}
1823
				$birthdays[sprintf('%04d%02d%02d',$year,$m,$d)][] = array(
1824
					'day'       => $d,
1825
					'month'     => $m,
1826
					'occurence' => 0,
1827
					'name'      => implode(' ', array_filter(array(lang('Birthday'),($pers['n_given'] ? $pers['n_given'] : $pers['n_prefix']), $pers['n_middle'],
1828
						$pers['n_family'], ($GLOBALS['egw_info']['server']['hide_birthdays'] == 'age' ? ($year - $y): '')))).
1829
						($y && in_array($GLOBALS['egw_info']['server']['hide_birthdays'], array('','age')) ? ' ('.$y.')' : ''),
1830
					'birthyear' => $y,	// this can be used to identify birthdays from holidays
1831
				);
1832
			}
1833
		}
1834
		Cache::setInstance(__CLASS__,"birthday-$year-$addressbook-".$GLOBALS['egw_info']['user']['preferences']['common']['lang'], $birthdays, self::BIRTHDAY_CACHE_TIME);
1835
		return $birthdays;
1836
	}
1837
1838
	/**
1839
	 * Called by delete-account hook, when an account get deleted --> deletes/moves the personal addressbook
1840
	 *
1841
	 * @param array $data
1842
	 */
1843
	function deleteaccount($data)
1844
	{
1845
		// delete/move personal addressbook
1846
		parent::deleteaccount($data);
1847
	}
1848
1849
	/**
1850
	 * Called by delete_category hook, when a category gets deleted.
1851
	 * Removes the category from addresses
1852
	 */
1853
	function delete_category($data)
1854
	{
1855
		// get all cats if you want to drop sub cats
1856
		$drop_subs = ($data['drop_subs'] && !$data['modify_subs']);
1857
		if($drop_subs)
1858
		{
1859
			$cats = new Categories('', 'addressbook');
1860
			$cat_ids = $cats->return_all_children($data['cat_id']);
1861
		}
1862
		else
1863
		{
1864
			$cat_ids = array($data['cat_id']);
1865
		}
1866
1867
		// Get addresses that use the category
1868
		@set_time_limit( 0 );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1868
		/** @scrutinizer ignore-unhandled */ @set_time_limit( 0 );

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1869
		foreach($cat_ids as $cat_id)
1870
		{
1871
			if (($ids = $this->search(array('cat_id' => $cat_id), false)))
1872
			{
1873
				foreach($ids as &$info)
1874
				{
1875
					$info['cat_id'] = implode(',',array_diff(explode(',',$info['cat_id']), $cat_ids));
1876
					$this->save($info);
1877
				}
1878
			}
1879
		}
1880
	}
1881
1882
	/**
1883
	 * Merges some given addresses into the first one and delete the others
1884
	 *
1885
	 * If one of the other addresses is an account, everything is merged into the account.
1886
	 * If two accounts are in $ids, the function fails (returns false).
1887
	 *
1888
	 * @param array $ids contact-id's to merge
1889
	 * @return int number of successful merged contacts, false on a fatal error (eg. cant merge two accounts)
1890
	 */
1891
	function merge($ids)
1892
	{
1893
		$this->error = false;
1894
		$account = null;
1895
		$custom_fields = Storage\Customfields::get('addressbook', true);
1896
		$custom_field_list = $this->read_customfields($ids);
1897
		foreach(parent::search(array('id'=>$ids),false) as $contact)	// $this->search calls the extended search from ui!
1898
		{
1899
			if ($contact['account_id'])
1900
			{
1901
				if (!is_null($account))
1902
				{
1903
					echo $this->error = 'Can not merge more then one account!';
1904
					return false;	// we dont deal with two accounts!
1905
				}
1906
				$account = $contact;
1907
				continue;
1908
			}
1909
			// Add in custom fields
1910
			if (is_array($custom_field_list[$contact['id']])) $contact = array_merge($contact, $custom_field_list[$contact['id']]);
1911
1912
			$pos = array_search($contact['id'],$ids);
1913
			$contacts[$pos] = $contact;
1914
		}
1915
		if (!is_null($account))	// we found an account, so we merge the contacts into it
1916
		{
1917
			$target = $account;
1918
			unset($account);
1919
		}
1920
		else					// we found no account, so we merge all but the first into the first
1921
		{
1922
			$target = $contacts[0];
1923
			unset($contacts[0]);
1924
		}
1925
		if (!$this->check_perms(Acl::EDIT,$target))
1926
		{
1927
			echo $this->error = 'No edit permission for the target contact!';
1928
			return 0;
1929
		}
1930
		foreach($contacts as $contact)
1931
		{
1932
			foreach($contact as $name => $value)
1933
			{
1934
				if (!$value) continue;
1935
1936
				switch($name)
1937
				{
1938
					case 'id':
1939
					case 'tid':
1940
					case 'owner':
1941
					case 'private':
1942
					case 'etag';
1943
						break;	// ignored
1944
1945
					case 'cat_id':	// cats are all merged together
1946
						if (!is_array($target['cat_id'])) $target['cat_id'] = $target['cat_id'] ? explode(',',$target['cat_id']) : array();
1947
						$target['cat_id'] = array_unique(array_merge($target['cat_id'],is_array($value)?$value:explode(',',$value)));
1948
						break;
1949
1950
					default:
1951
						// Multi-select custom fields can also be merged
1952
						if($name[0] == '#') {
1953
							$c_name = substr($name, 1);
1954
							if($custom_fields[$c_name]['type'] == 'select' && $custom_fields[$c_name]['rows'] > 1) {
1955
								if (!is_array($target[$name])) $target[$name] = $target[$name] ? explode(',',$target[$name]) : array();
1956
								$target[$name] = implode(',',array_unique(array_merge($target[$name],is_array($value)?$value:explode(',',$value))));
1957
							}
1958
						}
1959
						if (!$target[$name]) $target[$name] = $value;
1960
						break;
1961
				}
1962
			}
1963
1964
			// Merge distribution lists
1965
			$lists = $this->read_distributionlist(array($contact['id']));
1966
			foreach($lists[$contact['id']] as $list_id => $list_name)
1967
			{
1968
				parent::add2list($target['id'], $list_id);
1969
			}
1970
		}
1971
		if (!$this->save($target)) return 0;
1972
1973
		$success = 1;
1974
		foreach($contacts as $contact)
1975
		{
1976
			if (!$this->check_perms(Acl::DELETE,$contact))
1977
			{
1978
				continue;
1979
			}
1980
			foreach(Link::get_links('addressbook',$contact['id']) as $data)
1981
			{
1982
				//_debug_array(array('function'=>__METHOD__,'line'=>__LINE__,'app'=>'addressbook','id'=>$contact['id'],'data:'=>$data,'target'=>$target['id']));
1983
				// info_from and info_link_id (main link)
1984
				$newlinkID = Link::link('addressbook',$target['id'],$data['app'],$data['id'],$data['remark'],$target['owner']);
1985
				//_debug_array(array('newLinkID'=>$newlinkID));
1986
				if ($newlinkID)
1987
				{
1988
					// update egw_infolog set info_link_id=$newlinkID where info_id=$data['id'] and info_link_id=$data['link_id']
1989
					if ($data['app']=='infolog')
1990
					{
1991
						$this->db->update('egw_infolog',array(
1992
								'info_link_id' => $newlinkID
1993
							),array(
1994
								'info_id' => $data['id'],
1995
								'info_link_id' => $data['link_id']
1996
							),__LINE__,__FILE__,'infolog');
1997
					}
1998
					unset($newlinkID);
1999
				}
2000
			}
2001
			// Update calendar
2002
			$this->merge_calendar('c'.$contact['id'], $target['account_id'] ? 'u'.$target['account_id'] : 'c'.$target['id']);
2003
2004
			if ($this->delete($contact['id'])) $success++;
2005
		}
2006
		return $success;
2007
	}
2008
2009
	/**
2010
	 * Change the contact ID in any calendar events from the old contact ID
2011
	 * to the new merged ID
2012
	 *
2013
	 * @param int $old_id
2014
	 * @param int $new_id
2015
	 */
2016
	protected function merge_calendar($old_id, $new_id)
2017
	{
2018
		static $bo;
2019
		if(!is_object($bo))
2020
		{
2021
			$bo = new \calendar_boupdate();
2022
		}
2023
2024
		// Find all events with this contact
2025
		$events = $bo->search(array('users' => $old_id, 'ignore_acl' => true));
2026
2027
		foreach($events as $event)
2028
		{
2029
			$event['participants'][$new_id] = $event['participants'][$old_id];
2030
			unset($event['participants'][$old_id]);
2031
2032
			// Quietly update, ignoring ACL & no notifications
2033
			$bo->update($event, true, true, true, true, $messages, true);
2034
		}
2035
	}
2036
2037
	/**
2038
	 * Some caching for lists within request
2039
	 *
2040
	 * @var array
2041
	 */
2042
	private static $list_cache = array();
2043
2044
	/**
2045
	 * Check if user has required rights for a list or list-owner
2046
	 *
2047
	 * @param int $list
2048
	 * @param int $required
2049
	 * @param int $owner =null
2050
	 * @return boolean
2051
	 */
2052
	function check_list($list,$required,$owner=null)
2053
	{
2054
		if ($list && ($list_data = $this->read_list($list)))
2055
		{
2056
			$owner = $list_data['list_owner'];
2057
		}
2058
		//error_log(__METHOD__."($list, $required, $owner) grants[$owner]=".$this->grants[$owner]." returning ".array2string(!!($this->grants[$owner] & $required)));
2059
		return !!($this->grants[$owner] & $required);
2060
	}
2061
2062
	/**
2063
	 * Adds / updates a distribution list
2064
	 *
2065
	 * @param string|array $keys list-name or array with column-name => value pairs to specify the list
2066
	 * @param int $owner user- or group-id
2067
	 * @param array $contacts =array() contacts to add (only for not yet existing lists!)
2068
	 * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name'
2069
	 * @return int|boolean integer list_id or false on error
2070
	 */
2071
	function add_list($keys,$owner,$contacts=array(),array &$data=array())
2072
	{
2073
		if (!$this->check_list(null,Acl::ADD|Acl::EDIT,$owner)) return false;
2074
2075
		try {
2076
			$ret = parent::add_list($keys,$owner,$contacts,$data);
2077
			if ($ret) unset(self::$list_cache[$ret]);
0 ignored issues
show
introduced by
The condition $ret is always false.
Loading history...
2078
		}
2079
		// catch sql error, as creating same name&owner list gives a sql error doublicate key
2080
		catch(Db\Exception\InvalidSql $e) {
2081
			unset($e);	// not used
2082
			return false;
2083
		}
2084
		return $ret;
2085
	}
2086
2087
	/**
2088
	 * Adds contacts to a distribution list
2089
	 *
2090
	 * @param int|array $contact contact_id(s)
2091
	 * @param int $list list-id
2092
	 * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array()
2093
	 * @return false on error
2094
	 */
2095
	function add2list($contact,$list,array $existing=null)
2096
	{
2097
		if (!$this->check_list($list,Acl::EDIT)) return false;
2098
2099
		unset(self::$list_cache[$list]);
2100
2101
		return parent::add2list($contact,$list,$existing);
2102
	}
2103
2104
	/**
2105
	 * Removes one contact from distribution list(s)
2106
	 *
2107
	 * @param int|array $contact contact_id(s)
2108
	 * @param int $list list-id
2109
	 * @return false on error
2110
	 */
2111
	function remove_from_list($contact,$list=null)
2112
	{
2113
		if ($list && !$this->check_list($list,Acl::EDIT)) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $list of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2114
2115
		if ($list)
0 ignored issues
show
Bug Best Practice introduced by
The expression $list of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2116
		{
2117
			unset(self::$list_cache[$list]);
2118
		}
2119
		else
2120
		{
2121
			self::$list_cache = array();
2122
		}
2123
2124
		return parent::remove_from_list($contact,$list);
2125
	}
2126
2127
	/**
2128
	 * Deletes a distribution list (incl. it's members)
2129
	 *
2130
	 * @param int|array $list list_id(s)
2131
	 * @return number of members deleted or false if list does not exist
2132
	 */
2133
	function delete_list($list)
2134
	{
2135
		foreach((array)$list as $l)
2136
		{
2137
			if (!$this->check_list($l, Acl::DELETE)) return false;
2138
2139
			unset(self::$list_cache[$l]);
2140
		}
2141
2142
		return parent::delete_list($list);
2143
	}
2144
2145
	/**
2146
	 * Read data of a distribution list
2147
	 *
2148
	 * @param int $list list_id
2149
	 * @return array of data or false if list does not exist
2150
	 */
2151
	function read_list($list)
2152
	{
2153
		if (isset(self::$list_cache[$list])) return self::$list_cache[$list];
2154
2155
		return self::$list_cache[$list] = parent::read_list($list);
2156
	}
2157
2158
	/**
2159
	 * Get the address-format of a country
2160
	 *
2161
	 * This is a good reference where I got nearly all information, thanks to mikaelarhelger-AT-gmail.com
2162
	 * http://www.bitboost.com/ref/international-address-formats.html
2163
	 *
2164
	 * Mail me (RalfBecker-AT-outdoor-training.de) if you want your nation added or fixed.
2165
	 *
2166
	 * @param string $country
2167
	 * @return string 'city_state_postcode' (eg. US) or 'postcode_city' (eg. DE)
2168
	 */
2169
	function addr_format_by_country($country)
2170
	{
2171
		$code = Country::country_code($country);
2172
2173
		switch($code)
2174
		{
2175
			case 'AU':
2176
			case 'CA':
2177
			case 'GB':	// not exactly right, postcode is in separate line
2178
			case 'HK':	// not exactly right, they have no postcode
2179
			case 'IN':
2180
			case 'ID':
2181
			case 'IE':	// not exactly right, they have no postcode
2182
			case 'JP':	// not exactly right
2183
			case 'KR':
2184
			case 'LV':
2185
			case 'NZ':
2186
			case 'TW':
2187
			case 'SA':	// not exactly right, postcode is in separate line
2188
			case 'SG':
2189
			case 'US':
2190
				$adr_format = 'city_state_postcode';
2191
				break;
2192
2193
			case 'AR':
2194
			case 'AT':
2195
			case 'BE':
2196
			case 'CH':
2197
			case 'CZ':
2198
			case 'DK':
2199
			case 'EE':
2200
			case 'ES':
2201
			case 'FI':
2202
			case 'FR':
2203
			case 'DE':
2204
			case 'GL':
2205
			case 'IS':
2206
			case 'IL':
2207
			case 'IT':
2208
			case 'LT':
2209
			case 'LU':
2210
			case 'MY':
2211
			case 'MX':
2212
			case 'NL':
2213
			case 'NO':
2214
			case 'PL':
2215
			case 'PT':
2216
			case 'RO':
2217
			case 'RU':
2218
			case 'SE':
2219
				$adr_format = 'postcode_city';
2220
				break;
2221
2222
			default:
2223
				$adr_format = $this->prefs['addr_format'] ? $this->prefs['addr_format'] : 'postcode_city';
2224
		}
2225
		return $adr_format;
2226
	}
2227
2228
	/**
2229
	 * Find existing categories in database by name or add categories that do not exist yet
2230
	 * currently used for vcard import
2231
	 *
2232
	 * @param array $catname_list names of the categories which should be found or added
2233
	 * @param int $contact_id =null match against existing contact and expand the returned category ids
2234
	 *  by the ones the user normally does not see due to category permissions - used to preserve categories
2235
	 * @return array category ids (found, added and preserved categories)
2236
	 */
2237
	function find_or_add_categories($catname_list, $contact_id=null)
2238
	{
2239
		if ($contact_id && $contact_id > 0 && ($old_contact = $this->read($contact_id)))
0 ignored issues
show
Bug Best Practice introduced by
The expression $contact_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2240
		{
2241
			// preserve categories without users read access
2242
			$old_categories = explode(',',$old_contact['cat_id']);
2243
			$old_cats_preserve = array();
2244
			if (is_array($old_categories) && count($old_categories) > 0)
2245
			{
2246
				foreach ($old_categories as $cat_id)
2247
				{
2248
					if (!$this->categories->check_perms(Acl::READ, $cat_id))
2249
					{
2250
						$old_cats_preserve[] = $cat_id;
2251
					}
2252
				}
2253
			}
2254
		}
2255
2256
		$cat_id_list = array();
2257
		foreach ((array)$catname_list as $cat_name)
2258
		{
2259
			$cat_name = trim($cat_name);
2260
			$cat_id = $this->categories->name2id($cat_name, 'X-');
2261
			if (!$cat_id)
2262
			{
2263
				// some SyncML clients (mostly phones) add an X- to the category names
2264
				if (strncmp($cat_name, 'X-', 2) == 0)
2265
				{
2266
					$cat_name = substr($cat_name, 2);
2267
				}
2268
				$cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private'));
2269
			}
2270
2271
			if ($cat_id)
2272
			{
2273
				$cat_id_list[] = $cat_id;
2274
			}
2275
		}
2276
2277
		if (is_array($old_cats_preserve) && count($old_cats_preserve) > 0)
2278
		{
2279
			$cat_id_list = array_merge($cat_id_list, $old_cats_preserve);
2280
		}
2281
2282
		if (count($cat_id_list) > 1)
2283
		{
2284
			$cat_id_list = array_unique($cat_id_list);
2285
			sort($cat_id_list, SORT_NUMERIC);
2286
		}
2287
2288
		//error_log(__METHOD__."(".array2string($catname_list).", $contact_id) returning ".array2string($cat_id_list));
2289
		return $cat_id_list;
2290
	}
2291
2292
	function get_categories($cat_id_list)
2293
	{
2294
		if (!is_object($this->categories))
2295
		{
2296
			$this->categories = new Categories($this->user,'addressbook');
2297
		}
2298
2299
		if (!is_array($cat_id_list))
2300
		{
2301
			$cat_id_list = explode(',',$cat_id_list);
2302
		}
2303
		$cat_list = array();
2304
		foreach($cat_id_list as $cat_id)
2305
		{
2306
			if ($cat_id && $this->categories->check_perms(Acl::READ, $cat_id) &&
2307
					($cat_name = $this->categories->id2name($cat_id)) && $cat_name != '--')
2308
			{
2309
				$cat_list[] = $cat_name;
2310
			}
2311
		}
2312
2313
		return $cat_list;
2314
	}
2315
2316
	function fixup_contact(&$contact)
2317
	{
2318
		if (empty($contact['n_fn']))
2319
		{
2320
			$contact['n_fn'] = $this->fullname($contact);
2321
		}
2322
2323
		if (empty($contact['n_fileas']))
2324
		{
2325
			$contact['n_fileas'] = $this->fileas($contact);
2326
		}
2327
	}
2328
2329
	/**
2330
	 * Try to find a matching db entry
2331
	 *
2332
	 * @param array $contact   the contact data we try to find
2333
	 * @param boolean $relax =false if asked to relax, we only match against some key fields
2334
	 * @return array od matching contact_ids
2335
	 */
2336
	function find_contact($contact, $relax=false)
2337
	{
2338
		$empty_addr_one = $empty_addr_two = true;
2339
2340
		if ($this->log)
2341
		{
2342
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2343
				. '('. ($relax ? 'RELAX': 'EXACT') . ')[ContactData]:'
2344
				. array2string($contact)
2345
				. "\n", 3, $this->logfile);
2346
		}
2347
2348
		$matchingContacts = array();
2349
		if ($contact['id'] && ($found = $this->read($contact['id'])))
2350
		{
2351
			if ($this->log)
2352
			{
2353
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2354
					. '()[ContactID]: ' . $contact['id']
2355
					. "\n", 3, $this->logfile);
2356
			}
2357
			// We only do a simple consistency check
2358
			if (!$relax || ((empty($found['n_family']) || $found['n_family'] == $contact['n_family'])
2359
					&& (empty($found['n_given']) || $found['n_given'] == $contact['n_given'])
2360
					&& (empty($found['org_name']) || $found['org_name'] == $contact['org_name'])))
2361
			{
2362
				return array($found['id']);
2363
			}
2364
		}
2365
		unset($contact['id']);
2366
2367
		if (!$relax && !empty($contact['uid']))
2368
		{
2369
			if ($this->log)
2370
			{
2371
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2372
					. '()[ContactUID]: ' . $contact['uid']
2373
					. "\n", 3, $this->logfile);
2374
			}
2375
			// Try the given UID first
2376
			$criteria = array ('contact_uid' => $contact['uid']);
2377
			if (($foundContacts = parent::search($criteria)))
2378
			{
2379
				foreach ($foundContacts as $egwContact)
2380
				{
2381
					$matchingContacts[] = $egwContact['id'];
2382
				}
2383
			}
2384
			return $matchingContacts;
2385
		}
2386
		unset($contact['uid']);
2387
2388
		$columns_to_search = array('n_family', 'n_given', 'n_middle', 'n_prefix', 'n_suffix',
2389
						'bday', 'org_name', 'org_unit', 'title', 'role',
2390
						'email', 'email_home');
2391
		$tolerance_fields = array('n_middle', 'n_prefix', 'n_suffix',
2392
					  'bday', 'org_unit', 'title', 'role',
2393
					  'email', 'email_home');
2394
		$addr_one_fields = array('adr_one_street', 'adr_one_locality',
2395
					 'adr_one_region', 'adr_one_postalcode');
2396
		$addr_two_fields = array('adr_two_street', 'adr_two_locality',
2397
					 'adr_two_region', 'adr_two_postalcode');
2398
2399
		if (!empty($contact['owner']))
2400
		{
2401
			$columns_to_search += array('owner');
2402
		}
2403
2404
		$criteria = array();
2405
2406
		foreach ($columns_to_search as $field)
2407
		{
2408
			if ($relax && in_array($field, $tolerance_fields)) continue;
2409
2410
			if (empty($contact[$field]))
2411
			{
2412
				// Not every device supports all fields
2413
				if (!in_array($field, $tolerance_fields))
2414
				{
2415
					$criteria[$field] = '';
2416
				}
2417
			}
2418
			else
2419
			{
2420
				$criteria[$field] = $contact[$field];
2421
			}
2422
		}
2423
2424
		if (!$relax)
2425
		{
2426
			// We use addresses only for strong matching
2427
2428
			foreach ($addr_one_fields as $field)
2429
			{
2430
				if (empty($contact[$field]))
2431
				{
2432
					$criteria[$field] = '';
2433
				}
2434
				else
2435
				{
2436
					$empty_addr_one = false;
2437
					$criteria[$field] = $contact[$field];
2438
				}
2439
			}
2440
2441
			foreach ($addr_two_fields as $field)
2442
			{
2443
				if (empty($contact[$field]))
2444
				{
2445
					$criteria[$field] = '';
2446
				}
2447
				else
2448
				{
2449
					$empty_addr_two = false;
2450
					$criteria[$field] = $contact[$field];
2451
				}
2452
			}
2453
		}
2454
2455
		if ($this->log)
2456
		{
2457
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2458
				. '()[Addressbook FIND Step 1]: '
2459
				. 'CRITERIA = ' . array2string($criteria)
2460
				. "\n", 3, $this->logfile);
2461
		}
2462
2463
		// first try full match
2464
		if (($foundContacts = parent::search($criteria, true, '', '', '', true)))
2465
		{
2466
			foreach ($foundContacts as $egwContact)
2467
			{
2468
				$matchingContacts[] = $egwContact['id'];
2469
			}
2470
		}
2471
2472
		// No need for more searches for relaxed matching
2473
		if ($relax || count($matchingContacts)) return $matchingContacts;
2474
2475
2476
		if (!$empty_addr_one && $empty_addr_two)
2477
		{
2478
			// try given address and ignore the second one in EGW
2479
			foreach ($addr_two_fields as $field)
2480
			{
2481
				unset($criteria[$field]);
2482
			}
2483
2484
			if ($this->log)
2485
			{
2486
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2487
					. '()[Addressbook FIND Step 2]: '
2488
					. 'CRITERIA = ' . array2string($criteria)
2489
					. "\n", 3, $this->logfile);
2490
			}
2491
2492
			if (($foundContacts = parent::search($criteria, true, '', '', '', true)))
2493
			{
2494
				foreach ($foundContacts as $egwContact)
2495
				{
2496
					$matchingContacts[] = $egwContact['id'];
2497
				}
2498
			}
2499
			else
2500
			{
2501
				// try address as home address -- some devices don't qualify addresses
2502
				foreach ($addr_two_fields as $key => $field)
2503
				{
2504
					$criteria[$field] = $criteria[$addr_one_fields[$key]];
2505
					unset($criteria[$addr_one_fields[$key]]);
2506
				}
2507
2508
				if ($this->log)
2509
				{
2510
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2511
						. '()[Addressbook FIND Step 3]: '
2512
						. 'CRITERIA = ' . array2string($criteria)
2513
						. "\n", 3, $this->logfile);
2514
				}
2515
2516
				if (($foundContacts = parent::search($criteria, true, '', '', '', true)))
2517
				{
2518
					foreach ($foundContacts as $egwContact)
2519
					{
2520
						$matchingContacts[] = $egwContact['id'];
2521
					}
2522
				}
2523
			}
2524
		}
2525
		elseif (!$empty_addr_one && !$empty_addr_two)
2526
		{ // try again after address swap
2527
2528
			foreach ($addr_one_fields as $key => $field)
2529
			{
2530
				$_temp = $criteria[$field];
2531
				$criteria[$field] = $criteria[$addr_two_fields[$key]];
2532
				$criteria[$addr_two_fields[$key]] = $_temp;
2533
			}
2534
			if ($this->log)
2535
			{
2536
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2537
					. '()[Addressbook FIND Step 4]: '
2538
					. 'CRITERIA = ' . array2string($criteria)
2539
					. "\n", 3, $this->logfile);
2540
			}
2541
			if (($foundContacts = parent::search($criteria, true, '', '', '', true)))
2542
			{
2543
				foreach ($foundContacts as $egwContact)
2544
				{
2545
					$matchingContacts[] = $egwContact['id'];
2546
				}
2547
			}
2548
		}
2549
		if ($this->log)
2550
		{
2551
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2552
				. '()[FOUND]: ' . array2string($matchingContacts)
2553
				. "\n", 3, $this->logfile);
2554
		}
2555
		return $matchingContacts;
2556
	}
2557
2558
	/**
2559
	 * Get a ctag (collection tag) for one addressbook or all addressbooks readable by a user
2560
	 *
2561
	 * Currently implemented as maximum modification date (1 seconde granularity!)
2562
	 *
2563
	 * We have to include deleted entries, as otherwise the ctag will not change if an entry gets deleted!
2564
	 * (Only works if tracking of deleted entries / history is switched on!)
2565
	 *
2566
	 * @param int|array $owner =null 0=accounts, null=all addressbooks or integer account_id of user or group
2567
	 * @return string
2568
	 */
2569
	public function get_ctag($owner=null)
2570
	{
2571
		$filter = array('tid' => null);	// tid=null --> use all entries incl. deleted (tid='D')
2572
		// show addressbook of a single user?
2573
		if (!is_null($owner)) $filter['owner'] = $owner;
2574
2575
		// should we hide the accounts addressbook
2576
		if (!$owner && $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '1')
2577
		{
2578
			$filter['account_id'] = null;
2579
		}
2580
		$result = $this->search(array(),'contact_modified','contact_modified DESC','','',false,'AND',array(0,1),$filter);
2581
2582
		if (!$result || !isset($result[0]['modified']))
0 ignored issues
show
Bug Best Practice introduced by
The expression $result 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...
2583
		{
2584
			$ctag = 'empty';	// ctag for empty addressbook
2585
		}
2586
		else
2587
		{
2588
			// need to convert modified time back to server-time (was converted to user-time by search)
2589
			// as we use it direct in server-queries eg. CardDAV sync-report and to be consistent with CalDAV
2590
			$ctag = DateTime::user2server($result[0]['modified']);
2591
		}
2592
		//error_log(__METHOD__.'('.array2string($owner).') returning '.array2string($ctag));
2593
		return $ctag;
2594
	}
2595
2596
	/**
2597
	 * download photo of the given ($_GET['contact_id'] or $_GET['account_id']) contact
2598
	 */
2599
	function photo()
2600
	{
2601
		ob_start();
2602
2603
		$contact_id = isset($_GET['contact_id']) ? $_GET['contact_id'] :
2604
			(isset($_GET['account_id']) ? 'account:'.$_GET['account_id'] : 0);
2605
2606
		if (substr($contact_id,0,8) == 'account:')
2607
		{
2608
			$contact_id = $GLOBALS['egw']->accounts->id2name(substr($contact_id,8),'person_id');
2609
		}
2610
2611
		$contact = $this->read($contact_id);
2612
2613
		if (!($contact) ||
2614
			empty($contact['jpegphoto']) &&                           // LDAP/AD (not updated SQL)
2615
			!(($contact['files'] & \EGroupware\Api\Contacts::FILES_BIT_PHOTO) && // new SQL in VFS
2616
				($size = filesize($url= \EGroupware\Api\Link::vfs_path('addressbook', $contact_id, \EGroupware\Api\Contacts::FILES_PHOTO)))))
2617
		{
2618
			if (is_array($contact))
2619
			{
2620
				header('Content-type: image/jpeg');
2621
				$contact['jpegphoto'] =  \EGroupware\Api\avatar::lavatar(array(
2622
					'id' => $contact['id'],
2623
					'firstname' => $contact['n_given'],
2624
					'lastname' => $contact['n_family'])
2625
				);
2626
			}
2627
		}
2628
2629
		// use an etag over the image mapp
2630
		$etag = '"'.$contact_id.':'.$contact['etag'].'"';
2631
		if (!ob_get_contents())
2632
		{
2633
			header('Content-type: image/jpeg');
2634
			header('ETag: '.$etag);
2635
			// if etag parameter given in url, we can allow browser to cache picture via an Expires header
2636
			// different url with different etag parameter will force a reload
2637
			if (isset($_GET['etag']))
2638
			{
2639
				\EGroupware\Api\Session::cache_control(30*86400);	// cache for 30 days
2640
			}
2641
			// if servers send a If-None-Match header, response with 304 Not Modified, if etag matches
2642
			if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
2643
			{
2644
				header("HTTP/1.1 304 Not Modified");
2645
			}
2646
			elseif(!empty($contact['jpegphoto']))
2647
			{
2648
				header('Content-length: '.bytes($contact['jpegphoto']));
2649
				echo $contact['jpegphoto'];
2650
			}
2651
			else
2652
			{
2653
				header('Content-length: '.$size);
2654
				readfile($url);
2655
			}
2656
			exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2657
		}
2658
		Egw::redirect(\EGroupware\Api\Image::find('addressbook','photo'));
2659
	}
2660
}
2661