addressbook_ui::get_rows()   F
last analyzed

Complexity

Conditions 139

Size

Total Lines 446
Code Lines 245

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 139
eloc 245
nop 4
dl 0
loc 446
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware - Addressbook - user interface
4
 *
5
 * @link www.egroupware.org
6
 * @author Cornelius Weiss <[email protected]>
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @copyright (c) 2005-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9
 * @copyright (c) 2005/6 by Cornelius Weiss <[email protected]>
10
 * @package addressbook
11
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
12
 */
13
14
use EGroupware\Api;
15
use EGroupware\Api\Link;
16
use EGroupware\Api\Framework;
17
use EGroupware\Api\Egw;
18
use EGroupware\Api\Acl;
19
use EGroupware\Api\Vfs;
20
use EGroupware\Api\Etemplate;
21
22
/**
23
 * General user interface object of the adressbook
24
 */
25
class addressbook_ui extends addressbook_bo
26
{
27
	public $public_functions = array(
28
		'search'	=> True,
29
		'edit'		=> True,
30
		'view'		=> True,
31
		'index'     => True,
32
		'photo'		=> True,
33
		'emailpopup'=> True,
34
		'migrate2ldap' => True,
35
		'admin_set_fileas' => True,
36
		'admin_set_all_cleanup' => True,
37
		'cat_add' => True,
38
	);
39
	protected $org_views;
40
41
	/**
42
	 * Addressbook configuration (stored as phpgwapi = general server config)
43
	 *
44
	 * @var array
45
	 */
46
	protected $config;
47
48
	/**
49
	 * Fields to copy, default if nothing specified in config
50
	 *
51
	 * @var array
52
	 */
53
	static public $copy_fields = array(
54
		'org_name',
55
		'org_unit',
56
		'adr_one_street',
57
		'adr_one_street2',
58
		'adr_one_locality',
59
		'adr_one_region',
60
		'adr_one_postalcode',
61
		'adr_one_countryname',
62
		'adr_one_countrycode',
63
		'email',
64
		'url',
65
		'tel_work',
66
		'cat_id'
67
	);
68
69
	/**
70
	 * Instance of eTemplate class
71
	 *
72
	 * @var Etemplate
73
	 */
74
	protected $tmpl;
75
76
	/**
77
	 * Constructor
78
	 *
79
	 * @param string $contact_app
80
	 */
81
	function __construct($contact_app='addressbook')
82
	{
83
		parent::__construct($contact_app);
84
85
		$this->tmpl = new Etemplate();
86
87
		$this->grouped_views = array(
0 ignored issues
show
Bug Best Practice introduced by
The property grouped_views does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
88
			'org_name'                  => lang('Organisations'),
89
			'org_name,adr_one_locality' => lang('Organisations by location'),
90
			'org_name,org_unit'         => lang('Organisations by departments'),
91
			'duplicates'				=> lang('Duplicates')
92
		);
93
94
		// make sure the hook for export_limit is registered
95
		if (!Api\Hooks::exists('export_limit','addressbook')) Api\Hooks::read(true);
96
97
		$this->config =& $GLOBALS['egw_info']['server'];
98
99
		// check if a contact specific export limit is set, if yes use it also for etemplate's csv export
100
		$this->config['export_limit'] = $this->config['contact_export_limit'] = Api\Storage\Merge::getExportLimit($app='addressbook');
101
102
		if ($this->config['copy_fields'] && ($fields = is_array($this->config['copy_fields']) ?
103
			$this->config['copy_fields'] : unserialize($this->config['copy_fields'])))
104
		{
105
			// Set country code if country name is selected
106
			$supported_fields = $this->get_fields('supported',null,0);
107
			if(in_array('adr_one_countrycode', $supported_fields) && in_array('adr_one_countryname',$fields))
108
			{
109
				$fields[] = 'adr_one_countrycode';
110
			}
111
			if(in_array('adr_two_countrycode', $supported_fields) && in_array('adr_two_countryname',$fields))
112
			{
113
				$fields[] = 'adr_two_countrycode';
114
			}
115
116
			self::$copy_fields = $fields;
117
		}
118
	}
119
120
	/**
121
	 * List contacts of an addressbook
122
	 *
123
	 * @param array $_content =null submitted content
124
	 * @param string $msg =null	message to show
125
	 */
126
	function index($_content=null,$msg=null)
127
	{
128
		//echo "<p>uicontacts::index(".print_r($_content,true).",'$msg')</p>\n";
129
		if (($re_submit = is_array($_content)))
0 ignored issues
show
Unused Code introduced by
The assignment to $re_submit is dead and can be removed.
Loading history...
130
		{
131
132
			if (isset($_content['nm']['rows']['delete']))	// handle a single delete like delete with the checkboxes
133
			{
134
				$id = @key($_content['nm']['rows']['delete']);
135
				$_content['nm']['action'] = 'delete';
136
				$_content['nm']['selected'] = array($id);
137
			}
138
			if (isset($_content['nm']['rows']['document']))	// handle insert in default document button like an action
139
			{
140
				$id = @key($_content['nm']['rows']['document']);
141
				$_content['nm']['action'] = 'document';
142
				$_content['nm']['selected'] = array($id);
143
			}
144
			if ($_content['nm']['action'] !== '' && $_content['nm']['action'] !== null)
145
			{
146
				if (!count($_content['nm']['selected']) && !$_content['nm']['select_all'] && $_content['nm']['action'] != 'delete_list')
147
				{
148
					$msg = lang('You need to select some contacts first');
149
				}
150
				elseif ($_content['nm']['action'] == 'view_org' || $_content['nm']['action'] == 'view_duplicates')
151
				{
152
					// grouped view via context menu
153
					$_content['nm']['grouped_view'] = array_shift($_content['nm']['selected']);
154
				}
155
				else
156
				{
157
					$success = $failed = $action_msg = null;
158
					if ($this->action($_content['nm']['action'],$_content['nm']['selected'],$_content['nm']['select_all'],
159
						$success,$failed,$action_msg,'index',$msg,$_content['nm']['checkboxes']))
160
					{
161
						$msg .= lang('%1 contact(s) %2',$success,$action_msg);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $success. ( Ignorable by Annotation )

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

161
						$msg .= /** @scrutinizer ignore-call */ lang('%1 contact(s) %2',$success,$action_msg);

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...
162
						Framework::message($msg);
163
					}
164
					elseif(is_null($msg))
165
					{
166
						$msg .= lang('%1 contact(s) %2, %3 failed because of insufficent rights !!!',$success,$action_msg,$failed);
167
						Framework::message($msg,'error');
168
					}
169
					$msg = '';
170
				}
171
			}
172
			if ($_content['nm']['rows']['infolog'])
173
			{
174
				$org = key($_content['nm']['rows']['infolog']);
175
				return $this->infolog_org_view($org);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->infolog_org_view($org) targeting addressbook_ui::infolog_org_view() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
176
			}
177
			if ($_content['nm']['rows']['view'])	// show all contacts of an organisation
178
			{
179
				$grouped_view = key($_content['nm']['rows']['view']);
180
			}
181
			else
182
			{
183
				$grouped_view = $_content['nm']['grouped_view'];
184
			}
185
			$typeselection = $_content['nm']['col_filter']['tid'];
186
		}
187
		elseif($_GET['add_list'])
188
		{
189
			$list = $this->add_list($_GET['add_list'],$_GET['owner']?$_GET['owner']:$this->user);
190
			if ($list === true)
0 ignored issues
show
introduced by
The condition $list === true is always false.
Loading history...
191
			{
192
				$msg = lang('List already exists!');
193
			}
194
			elseif ($list)
0 ignored issues
show
introduced by
The condition $list is always false.
Loading history...
195
			{
196
				$msg = lang('List created');
197
			}
198
			else
199
			{
200
				$msg = lang('List creation failed, no rights!');
201
			}
202
		}
203
		$preserv = array();
204
		$content = array();
205
		if($msg || $_GET['msg'])
206
		{
207
			Framework::message($msg ? $msg : $_GET['msg']);
208
		}
209
210
		$content['nm'] = Api\Cache::getSession('addressbook', 'index');
211
		if (!is_array($content['nm']))
212
		{
213
			$content['nm'] = array(
214
				'get_rows'       =>	'addressbook.addressbook_ui.get_rows',	// I  method/callback to request the data for the rows eg. 'notes.bo.get_rows'
215
				'bottom_too'     => false,		// I  show the nextmatch-line (arrows, filters, search, ...) again after the rows
216
				'never_hide'     => True,		// I  never hide the nextmatch-line if less then maxmatch entrie
217
				'start'          =>	0,			// IO position in list
218
				'cat_id'         =>	'',			// IO category, if not 'no_cat' => True
219
				'search'         =>	'',			// IO search pattern
220
				'order'          =>	'n_family',	// IO name of the column to sort after (optional for the sortheaders)
221
				'sort'           =>	'ASC',		// IO direction of the sort: 'ASC' or 'DESC'
222
				'col_filter'     =>	array(),	// IO array of column-name value pairs (optional for the filterheaders)
223
				//'cat_id_label' => lang('Categories'),
224
				//'filter_label' => lang('Addressbook'),	// I  label for filter    (optional)
225
				'filter'         =>	'',	// =All	// IO filter, if not 'no_filter' => True
226
				'filter_no_lang' => True,		// I  set no_lang for filter (=dont translate the options)
227
				'no_filter2'     => True,		// I  disable the 2. filter (params are the same as for filter)
228
				//'filter2_label'=>	lang('Distribution lists'),			// IO filter2, if not 'no_filter2' => True
229
				'filter2'        =>	'',			// IO filter2, if not 'no_filter2' => True
230
				'filter2_no_lang'=> True,		// I  set no_lang for filter2 (=dont translate the options)
231
				'lettersearch'   => true,
232
				// using a positiv list now, as we constantly adding new columns in addressbook, but not removing them from default
233
				'default_cols'   => 'type,n_fileas_n_given_n_family_n_family_n_given_org_name_n_family_n_given_n_fileas,'.
234
					'number,org_name,org_unit,'.
235
					'business_adr_one_countrycode_adr_one_postalcode,tel_work_tel_cell_tel_home,url_email_email_home',
236
				/* old negative list
237
				'default_cols'   => '!cat_id,contact_created_contact_modified,distribution_list,contact_id,owner,room',*/
238
				'filter2_onchange' => "return app.addressbook.filter2_onchange();",
239
				'filter2_tags'	=> true,
240
				//'actions'        => $this->get_actions(),		// set on each request, as it depends on some filters
241
				'row_id'         => 'id',
242
				'row_modified'   => 'modified',
243
				'is_parent'      => 'group_count',
244
				'parent_id'      => 'parent_id',
245
				'favorites'      => true,
246
			);
247
248
			// use the state of the last session stored in the user prefs
249
			if (($state = @unserialize($this->prefs['index_state'])))
250
			{
251
				$content['nm'] = array_merge($content['nm'],$state);
252
			}
253
		}
254
		$sel_options['cat_id'] = array('' => lang('All categories'), '0' => lang('None'));
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
255
256
		$content['nm']['placeholder_actions'] = array('add');
257
		// Edit and delete list actions depends on permissions
258
		if($this->get_lists(Acl::EDIT))
259
		{
260
			$content['nm']['placeholder_actions'][] = 'rename_list';
261
			$content['nm']['placeholder_actions'][] = 'delete_list';
262
		}
263
264
		// Search parameter passed in
265
		if ($_GET['search']) {
266
			$content['nm']['search'] = $_GET['search'];
267
		}
268
		if (isset($typeselection)) $content['nm']['col_filter']['tid'] = $typeselection;
269
		// save the tid for use in creating new addressbook entrys via UI. Current tid is to be used as type of new entrys
270
		//error_log(__METHOD__.__LINE__.' '.$content['nm']['col_filter']['tid']);
271
		Api\Cache::setSession('addressbook','active_tid',$content['nm']['col_filter']['tid']);
272
		if ($this->lists_available())
273
		{
274
			$sel_options['filter2'] = $this->get_lists(Acl::READ,array('' => lang('No distribution list')));
275
			$sel_options['filter2']['add'] = lang('Add a new list').'...';	// put it at the end
276
		}
277
278
		// Organisation stuff is not (yet) availible with ldap
279
		if($GLOBALS['egw_info']['server']['contact_repository'] != 'ldap')
280
		{
281
			$content['nm']['header_left'] = 'addressbook.index.left';
282
		}
283
		$sel_options['filter'] = $sel_options['owner'] = $this->get_addressbooks(Acl::READ, lang('All addressbooks'));
284
		$sel_options['to'] = array(
285
			'to'  => 'To',
286
			'cc'  => 'Cc',
287
			'bcc' => 'Bcc',
288
		);
289
		$sel_options['adr_one_countrycode']['-custom-'] = lang('No country selected');
290
291
		// if there is any export limit set, pass it on to the nextmatch, to be evaluated by the export
292
		if (isset($this->config['contact_export_limit']) && (int)$this->config['contact_export_limit']) $content['nm']['export_limit']=$this->config['contact_export_limit'];
293
294
		// Merge to email dialog needs the infolog types
295
		$infolog = new infolog_bo();
296
		$sel_options['info_type'] = $infolog->enums['type'];
297
298
		// dont show tid-selection if we have only one content_type
299
		// be a bit more sophisticated about it
300
		$availabletypes = array_keys($this->content_types);
301
		if ($content['nm']['col_filter']['tid'] && !in_array($content['nm']['col_filter']['tid'],$availabletypes))
302
		{
303
			//_debug_array(array('Typefilter:'=> $content['nm']['col_filter']['tid'],'Available Types:'=>$availabletypes,'action:'=>'remove invalid filter'));
304
			unset($content['nm']['col_filter']['tid']);
305
		}
306
		if (!isset($content['nm']['col_filter']['tid'])) $content['nm']['col_filter']['tid'] = $availabletypes[0];
307
		if (count($this->content_types) > 1)
308
		{
309
			foreach($this->content_types as $tid => $data)
310
			{
311
				$sel_options['tid'][$tid] = $data['name'];
312
			}
313
		}
314
		else
315
		{
316
			$this->tmpl->disableElement('nm[col_filter][tid]');
317
		}
318
		// get the availible grouped-views plus the label of the contacts view of one group
319
		$sel_options['grouped_view'] = $this->grouped_views;
320
		if (isset($grouped_view))
321
		{
322
			$content['nm']['grouped_view'] = $grouped_view;
323
		}
324
325
		$content['nm']['actions'] = $this->get_actions($content['nm']['col_filter']['tid']);
326
327
		if (!isset($sel_options['grouped_view'][(string) $content['nm']['grouped_view']]))
328
		{
329
			$sel_options['grouped_view'] += $this->_get_grouped_name((string)$content['nm']['grouped_view']);
330
		}
331
		// unset the filters regarding grouped views, when there is no group selected
332
		if (empty($sel_options['grouped_view'][(string) $content['nm']['grouped_view']]) || stripos($grouped_view,":") === false )
333
		{
334
			$this->unset_grouped_filters($content['nm']);
335
		}
336
		$content['nm']['grouped_view_label'] = $sel_options['grouped_view'][(string) $content['nm']['grouped_view']];
337
338
		$this->tmpl->read('addressbook.index');
339
		return $this->tmpl->exec('addressbook.addressbook_ui.index',
340
			$content,$sel_options,array(),$preserv);
341
	}
342
343
	/**
344
	 * Get actions / context menu items
345
	 *
346
	 * @param string $tid_filter =null
347
	 * @return array see Etemplate\Widget\Nextmatch::get_actions()
348
	 */
349
	public function get_actions($tid_filter=null)
350
	{
351
		// Contact view
352
		$actions = array(
353
			'view' => array(
354
				'caption' => 'CRM-View',
355
				'default' => $GLOBALS['egw_info']['user']['preferences']['addressbook']['crm_list'] != '~edit~',
356
				'allowOnMultiple' => false,
357
				'group' => $group=1,
358
				'onExecute' => 'javaScript:app.addressbook.view',
359
				'enableClass' => 'contact_contact',
360
				'hideOnDisabled' => true,
361
				// Children added below
362
				'children' => array(),
363
				'hideOnMobile' => true
364
			),
365
			'open' => array(
366
				'caption' => 'Open',
367
				'default' => $GLOBALS['egw_info']['user']['preferences']['addressbook']['crm_list'] == '~edit~',
368
				'allowOnMultiple' => false,
369
				'enableClass' => 'contact_contact',
370
				'hideOnDisabled' => true,
371
				'url' => 'menuaction=addressbook.addressbook_ui.edit&contact_id=$id',
372
				'popup' => Link::get_registry('addressbook', 'edit_popup'),
373
				'group' => $group,
374
			),
375
			'add' => array(
376
				'caption' => 'Add',
377
				'group' => $group,
378
				'hideOnDisabled' => true,
379
				'children' => array(
380
					'new' => array(
381
						'caption' => 'New',
382
						'url' => 'menuaction=addressbook.addressbook_ui.edit',
383
						'popup' => Link::get_registry('addressbook', 'add_popup'),
384
						'icon' => 'new',
385
					),
386
					'copy' => array(
387
						'caption' => 'Copy',
388
						'url' => 'menuaction=addressbook.addressbook_ui.edit&makecp=1&contact_id=$id',
389
						'popup' => Link::get_registry('addressbook', 'add_popup'),
390
						'enableClass' => 'contact_contact',
391
						'allowOnMultiple' => false,
392
						'icon' => 'copy',
393
					),
394
				),
395
				'hideOnMobile' => true
396
			),
397
		);
398
		// CRM view options
399
		$crm_count = 0;
400
		$crm_apps = array('infolog','tracker');
401
		foreach($crm_apps as $app)
402
		{
403
			if ($GLOBALS['egw_info']['user']['apps'][$app]) $crm_count++;
404
		}
405
		if($GLOBALS['egw_info']['user']['apps']['infolog'])
406
		{
407
			array_splice($crm_apps, 1, 0, 'infolog-organisation');
408
		}
409
		if($crm_count > 1)
410
		{
411
			foreach($crm_apps as $app)
412
			{
413
				$actions['view']['children']["view-$app"] = array(
414
					'caption' => $app,
415
					'icon' => "$app/navbar"
416
				);
417
			}
418
		}
419
420
		// org view
421
		$actions += array(
422
			'view_org' => array(
423
				'caption' => 'View',
424
				'default' => true,
425
				'allowOnMultiple' => false,
426
				'group' => $group=1,
427
				'enableClass' => 'contact_organisation',
428
				'hideOnDisabled' => true
429
			),
430
			'add_org' => array(
431
				'caption' => 'Add',
432
				'group' => $group,
433
				'allowOnMultiple' => false,
434
				'enableClass' => 'contact_organisation',
435
				'hideOnDisabled' => true,
436
				'url' => 'menuaction=addressbook.addressbook_ui.edit&org=$id',
437
				'popup' => Link::get_registry('addressbook', 'add_popup'),
438
			),
439
		);
440
441
		// Duplicates view
442
		$actions += array(
443
			'view_duplicates' => array(
444
				'caption' => 'View',
445
				'default' => true,
446
				'allowOnMultiple' => false,
447
				'group' => $group=1,
448
				'enableClass' => 'contact_duplicate',
449
				'hideOnDisabled' => true
450
			)
451
		);
452
453
		++$group;	// other AB related stuff group: lists, AB's, categories
454
		// categories submenu
455
		$actions['cat'] = array(
456
			'caption' => 'Categories',
457
			'group' => $group,
458
			'children' => array(
459
				'cat_add' => Etemplate\Widget\Nextmatch::category_action(
460
					'addressbook',$group,'Add category', 'cat_add_',
461
					true, 0,Etemplate\Widget\Nextmatch::DEFAULT_MAX_MENU_LENGTH,false
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\Etemplate...atch::category_action() has too many arguments starting with false. ( Ignorable by Annotation )

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

461
				'cat_add' => Etemplate\Widget\Nextmatch::/** @scrutinizer ignore-call */ category_action(

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...
462
				)+array(
463
					'icon' => 'foldertree_nolines_plus',
464
					'disableClass' => 'rowNoEdit',
465
				),
466
				'cat_del' => Etemplate\Widget\Nextmatch::category_action(
467
					'addressbook',$group,'Delete category', 'cat_del_',
468
					true, 0,Etemplate\Widget\Nextmatch::DEFAULT_MAX_MENU_LENGTH,false
469
				)+array(
470
					'icon' => 'foldertree_nolines_minus',
471
					'disableClass' => 'rowNoEdit',
472
				),
473
			),
474
		);
475
		if (!$GLOBALS['egw_info']['user']['apps']['preferences']) unset($actions['cats']['children']['cat_edit']);
476
		// Submenu for all distributionlist stuff
477
		$actions['lists'] = array(
478
			'caption' => 'Distribution lists',
479
			'children' => array(
480
				'list_add' => array(
481
					'caption' => 'Add a new list',
482
					'icon' => 'new',
483
					'onExecute' => 'javaScript:app.addressbook.add_new_list',
484
				),
485
			),
486
			'group' => $group,
487
		);
488
		if (($add_lists = $this->get_lists(Acl::EDIT)))	// do we have distribution lists?, and are we allowed to edit them
489
		{
490
			$actions['lists']['children'] += array(
491
				'to_list' => array(
492
					'caption' => 'Add to distribution list',
493
					'children' => $add_lists,
494
					'prefix' => 'to_list_',
495
					'icon' => 'foldertree_nolines_plus',
496
					'enabled' => ($add_lists?true:false), // if there are editable lists, allow to add a contact to one of them,
0 ignored issues
show
introduced by
$add_lists is a non-empty array, thus is always true.
Loading history...
497
					//'disableClass' => 'rowNoEdit',	  // wether you are allowed to edit the contact or not, as you alter a list, not the contact
498
				),
499
				'remove_from_list' => array(
500
					'caption' => 'Remove from distribution list',
501
					'confirm' => 'Remove selected contacts from distribution list',
502
					'icon' => 'foldertree_nolines_minus',
503
					'enabled' => 'javaScript:app.addressbook.nm_compare_field',
504
					'fieldId' => 'exec[nm][filter2]',
505
					'fieldValue' => '!',	// enable if list != ''
506
				),
507
				'rename_list' => array(
508
					'caption' => 'Rename selected distribution list',
509
					'icon' => 'edit',
510
					'enabled' => 'javaScript:app.addressbook.nm_compare_field',
511
					'fieldId' => 'exec[nm][filter2]',
512
					'fieldValue' => '!',	// enable if list != ''
513
					'onExecute' => 'javaScript:app.addressbook.rename_list'
514
				),
515
				'delete_list' => array(
516
					'caption' => 'Delete selected distribution list!',
517
					'confirm' => 'Delete selected distribution list!',
518
					'icon' => 'delete',
519
					'enabled' => 'javaScript:app.addressbook.nm_compare_field',
520
					'fieldId' => 'exec[nm][filter2]',
521
					'fieldValue' => '!',	// enable if list != ''
522
				),
523
			);
524
			if(is_subclass_of('etemplate', 'etemplate_new'))
525
			{
526
				$actions['lists']['children']['remove_from_list']['fieldId'] = 'filter2';
527
				$actions['lists']['children']['rename_list']['fieldId'] = 'filter2';
528
				$actions['lists']['children']['delete_list']['fieldId'] = 'filter2';
529
			}
530
		}
531
		// move to AB
532
		if (($move2addressbooks = $this->get_addressbooks(Acl::ADD)))	// do we have addressbooks, we should
533
		{
534
			unset($move2addressbooks[0]);	// do not offer action to move contact to an account, as we dont support that currrently
535
			foreach($move2addressbooks as $owner => $label)
536
			{
537
				$icon = $type_label = null;
538
				$this->type_icon((int)$owner, substr($owner,-1) == 'p', 'n', $icon, $type_label);
539
				$move2addressbooks[$owner] = array(
540
					'icon' => $icon,
541
					'caption' => $label,
542
				);
543
			}
544
			// copy checkbox
545
			$move2addressbooks= array(
546
				'copy' =>array(
547
					'id' => 'move_to_copy',
548
					'caption' => 'Copy instead of move',
549
					'checkbox' => true,
550
				)) + $move2addressbooks;
551
			$actions['move_to'] = array(
552
				'caption' => 'Move to addressbook',
553
				'children' => $move2addressbooks,
554
				'prefix' => 'move_to_',
555
				'group' => $group,
556
				'disableClass' => 'rowNoDelete',
557
				'hideOnMobile' => true
558
			);
559
		}
560
		$actions['merge'] = array(
561
			'caption' => 'Merge contacts',
562
			'confirm' => 'Merge into first or account, deletes all other!',
563
			'hint' => 'Merge into first or account, deletes all other!',
564
			'allowOnMultiple' => 'only',
565
			'group' => $group,
566
			'hideOnMobile' => true,
567
			'enabled' => 'javaScript:app.addressbook.can_merge'
568
		);
569
		// Duplicates view
570
		$actions['merge_duplicates'] = array(
571
			'caption'	=> 'Merge duplicates',
572
			'group'		=> $group,
573
			'allowOnMultiple'	=> true,
574
			'enableClass' => 'contact_duplicate',
575
			'hideOnDisabled'	=> true
576
		);
577
578
		++$group;	// integration with other apps: infolog, calendar, filemanager, messenger
579
580
		// Integrate Messenger app actions for adding new buddy into roster list
581
		if ($GLOBALS['egw_info']['user']['apps']['messenger'])
582
		{
583
			$actions['messenger_app'] = array(
584
				'caption' => 'Messenger',
585
				'icon' => 'messenger/navbar',
586
				'group' => $group,
587
				'children' => array(
588
					'addBuddy' => array(
589
						'caption' => lang('Add contact'),
590
						'icon' => 'add',
591
						'allowOnMultiple' => true,
592
						'onExecute' => 'javaScript:app.messenger.addBuddy',
593
						'enabled' => 'javaScript:app.messenger.addBuddyEnabaled'
594
					)
595
				)
596
			);
597
		}
598
599
		if ($GLOBALS['egw_info']['user']['apps']['infolog'])
600
		{
601
			$actions['infolog_app'] = array(
602
				'caption' => 'InfoLog',
603
				'icon' => 'infolog/navbar',
604
				'group' => $group,
605
				'children' => array(
606
					'infolog' => array(
607
						'caption' => lang('View linked InfoLog entries'),
608
						'icon' => 'infolog/navbar',
609
						'onExecute' => 'javaScript:app.addressbook.view_infolog',
610
						'disableClass' => 'contact_duplicate',
611
						'allowOnMultiple' => true,
612
						'hideOnDisabled' => true,
613
					),
614
					'infolog_add' => array(
615
						'caption' => 'Add a new Infolog',
616
						'icon' => 'new',
617
						'url' => 'menuaction=infolog.infolog_ui.edit&type=task&action=addressbook&action_id=$id',
618
						'popup' => Link::get_registry('infolog', 'add_popup'),
619
						'onExecute' => 'javaScript:app.addressbook.add_task',	// call server for org-view only
620
					),
621
				),
622
				'hideOnMobile' => true
623
			);
624
		}
625
		if ($GLOBALS['egw_info']['user']['apps']['calendar'])
626
		{
627
			$actions['calendar'] = array(
628
				'icon' => 'calendar/navbar',
629
				'caption' => 'Calendar',
630
				'group' => $group,
631
				'enableClass' => 'contact_contact',
632
				'children' => array(
633
					'calendar_view' => array(
634
						'caption' => 'Show',
635
						'icon' => 'view',
636
						'onExecute' => 'javaScript:app.addressbook.view_calendar',
637
						'targetapp' => 'calendar',	// open in calendar tab,
638
						'hideOnDisabled' => true,
639
					),
640
					'calendar_add' => array(
641
						'caption' => 'Add appointment',
642
						'icon' => 'new',
643
						'popup' => Link::get_registry('calendar', 'add_popup'),
644
						'onExecute' => 'javaScript:app.addressbook.add_cal',
645
					),
646
				),
647
				'hideOnMobile' => true
648
			);
649
		}
650
		//Send to email
651
		$actions['email'] = array(
652
				'caption' => 'Email',
653
				'icon'	=> 'mail/navbar',
654
				'enableClass' => 'contact_contact',
655
				'hideOnDisabled' => true,
656
				'group' => $group,
657
				'children' => array(
658
						'add_to_to' => array(
659
							'caption' => lang('Add to To'),
660
							'no_lang' => true,
661
							'onExecute' => 'javaScript:app.addressbook.addEmail',
662
663
						),
664
						'add_to_cc' => array(
665
							'caption' => lang('Add to Cc'),
666
							'no_lang' => true,
667
							'onExecute' => 'javaScript:app.addressbook.addEmail',
668
669
						),
670
						'add_to_bcc' => array(
671
							'caption' => lang('Add to BCc'),
672
							'no_lang' => true,
673
							'onExecute' => 'javaScript:app.addressbook.addEmail',
674
675
						),
676
						'email_business' => array(
677
							'caption' => lang('Business email'),
678
							'no_lang' => true,
679
							'checkbox' => true,
680
							'group'	=> $group,
681
							'onExecute' => 'javaScript:app.addressbook.mailCheckbox',
682
							'checked' => $this->prefs['preferredMail']['business'],
683
						),
684
						'email_home' => array(
685
							'caption' => lang('Home email'),
686
							'no_lang' => true,
687
							'checkbox' => true,
688
							'group'	=> $group,
689
							'onExecute' => 'javaScript:app.addressbook.mailCheckbox',
690
							'checked' => $this->prefs['preferredMail']['private'],
691
						),
692
				),
693
694
			);
695
		if (!$this->prefs['preferredMail'])
696
			$actions['email']['children']['email_business']['checked'] = true;
697
698
		if ($GLOBALS['egw_info']['user']['apps']['filemanager'])
699
		{
700
			$actions['filemanager'] = array(
701
				'icon' => 'filemanager/navbar',
702
				'caption' => 'Filemanager',
703
				'url' => 'menuaction=filemanager.filemanager_ui.index&path=/apps/addressbook/$id&ajax=true',
704
				'allowOnMultiple' => false,
705
				'group' => $group,
706
				// disable for for group-views, as it needs contact-ids
707
				'enableClass' => 'contact_contact',
708
				'hideOnMobile' => true
709
			);
710
		}
711
712
		$actions['geolocation'] = array(
713
			'caption' => 'GeoLocation',
714
			'icon' => 'map',
715
			'group' => ++$group,
716
			'enableClass' => 'contact_contact',
717
			'children' => array (
718
				'private' => array(
719
					'caption' => 'Private Address',
720
					'enabled' => 'javaScript:app.addressbook.geoLocation_enabled',
721
					'onExecute' => 'javaScript:app.addressbook.geoLocationExec',
722
723
				),
724
				'business' => array(
725
					'caption' => 'Business Address',
726
					'enabled' => 'javaScript:app.addressbook.geoLocation_enabled',
727
					'onExecute' => 'javaScript:app.addressbook.geoLocationExec',
728
729
				)
730
			)
731
		);
732
		$actions += EGroupware\Api\Link\Sharing::get_actions('addressbook', $group);
733
734
		// check if user is an admin or the export is not generally turned off (contact_export_limit is non-numerical, eg. no)
735
		$exception = Api\Storage\Merge::is_export_limit_excepted();
736
		if ((isset($GLOBALS['egw_info']['user']['apps']['admin']) || $exception)  || !$this->config['contact_export_limit'] || (int)$this->config['contact_export_limit'])
737
		{
738
			$actions['export'] = array(
739
				'caption' => 'Export',
740
				'icon' => 'filesave',
741
				'enableClass' => 'contact_contact',
742
				'group' => ++$group,
743
				'children' => array(
744
					'csv'    => array(
745
						'caption' => 'Export as CSV',
746
						'allowOnMultiple' => true,
747
						'url' => 'menuaction=importexport.importexport_export_ui.export_dialog&appname=addressbook&plugin=addressbook_export_contacts_csv&selection=$id&select_all=$select_all',
748
						'popup' => '850x440'
749
					),
750
					'vcard'  => array(
751
						'caption' => 'Export as VCard',
752
						'postSubmit' => true,	// download needs post submit (not Ajax) to work
753
						'icon' => Vfs::mime_icon('text/vcard'),
754
					),
755
				),
756
				'hideOnMobile' => true
757
			);
758
		}
759
760
		$actions['documents'] = Api\Contacts\Merge::document_action(
761
			$this->prefs['document_dir'], $group, 'Insert in document', 'document_',
762
			$this->prefs['default_document'], $this->config['contact_export_limit']
763
		);
764
		if ($GLOBALS['egw_info']['user']['apps']['felamimail']||$GLOBALS['egw_info']['user']['apps']['mail'])
765
		{
766
			$actions['mail'] = array(
767
				'caption' => lang('Mail VCard'),
768
				'icon' => 'filemanager/mail_post_to',
769
				'group' => $group,
770
				'onExecute' => 'javaScript:app.addressbook.adb_mail_vcard',
771
				'enableClass' => 'contact_contact',
772
				'hideOnDisabled' => true,
773
				'hideOnMobile' => true,
774
				'disableIfNoEPL' => true
775
			);
776
		}
777
		++$group;
778
		if (!($tid_filter == 'D' && !$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge'))
779
		{
780
			$actions['delete'] = array(
781
				'caption' => 'Delete',
782
				'confirm' => 'Delete this contact',
783
				'confirm_multiple' => 'Delete these entries',
784
				'group' => $group,
785
				'disableClass' => 'rowNoDelete',
786
			);
787
		}
788
		if ($this->grants[0] & Acl::DELETE)
789
		{
790
			$actions['delete_account'] = array(
791
				'caption' => 'Delete',
792
				'icon' => 'delete',
793
				'group' => $group,
794
				'enableClass' => 'rowAccount',
795
				'hideOnDisabled' => true,
796
				'popup' => '400x200',
797
				'url' => 'menuaction=admin.admin_account.delete&contact_id=$id',
798
			);
799
			$actions['delete']['hideOnDisabled'] = true;
800
		}
801
		if($tid_filter == 'D')
802
		{
803
			$actions['undelete'] = array(
804
				'caption' => 'Un-delete',
805
				'icon' => 'revert',
806
				'group' => $group,
807
				'disableClass' => 'rowNoEdit',
808
			);
809
		}
810
		if (isset($actions['export']['children']['csv']) &&
811
			(!isset($GLOBALS['egw_info']['user']['apps']['importexport']) ||
812
			!importexport_helper_functions::has_definitions('addressbook','export')))
813
		{
814
			unset($actions['export']['children']['csv']);
815
		}
816
		// Intercept open action in order to open entry into view mode instead of edit
817
		if (Api\Header\UserAgent::mobile())
818
		{
819
			$actions['open']['onExecute'] = 'javaScript:app.addressbook.viewEntry';
820
			$actions['open']['mobileViewTemplate'] = 'view?'.filemtime(Api\Etemplate\Widget\Template::rel2path('/addressbook/templates/mobile/view.xet'));
821
			$actions['view']['default'] = false;
822
			$actions['open']['default'] = true;
823
		}
824
		// Allow contacts to be dragged
825
		/*
826
		$actions['drag'] = array(
827
			'type' => 'drag',
828
			'dragType' => 'addressbook'
829
		);
830
		*/
831
		return $actions;
832
	}
833
834
	/**
835
	 * Get a nice name for the given grouped view ID
836
	 *
837
	 * @param String $view_id Some kind of indicator for a specific group, either
838
	 *	organisation or duplicate.  It looks like key:value pairs seperated by |||.
839
	 *
840
	 * @return Array(ID => name), where ID is the $view_id passed in
841
	 */
842
	protected function _get_grouped_name($view_id)
843
	{
844
		$group_name = array();
845
		if (strpos($view_id,'*AND*')!== false) $view_id = str_replace('*AND*','&',$view_id);
846
		foreach(explode('|||',$view_id) as $part)
847
		{
848
			list(,$name) = explode(':',$part,2);
849
			if ($name) $group_name[] = $name;
850
		}
851
		$name = implode(': ',$group_name);
852
		return $name ? array($view_id => $name) : array();
853
	}
854
855
	/**
856
	 * Unset the relevant column filters to clear a grouped view
857
	 *
858
	 * @param Array $query
859
	 */
860
	protected function unset_grouped_filters(&$query)
861
	{
862
		unset($query['col_filter']['org_name']);
863
		unset($query['col_filter']['org_unit']);
864
		unset($query['col_filter']['adr_one_locality']);
865
		foreach(array_keys(static::$duplicate_fields) as $field)
866
		{
867
			unset($query['col_filter'][$field]);
868
		}
869
	}
870
871
	/**
872
	 * Adjust the query as needed and get the rows for the grouped views (organisation
873
	 * or duplicate contacts)
874
	 *
875
	 * @param Array $query Nextmatch query
876
	 * @return array rows found
877
	 */
878
	protected function get_grouped_rows(&$query)
879
	{
880
		// Query doesn't like empties
881
		unset($query['col_filter']['parent_id']);
882
883
		if($query['actions'] && $query['actions']['open'])
884
		{
885
			// Just switched from contact view, update actions
886
			$query['actions'] = $this->get_actions($query['col_filter']['tid']);
887
		}
888
889
		$query['template'] = $query['grouped_view'] == 'duplicates' ? 'addressbook.index.duplicate_rows' : 'addressbook.index.org_rows';
890
891
		if ($query['advanced_search'])
892
		{
893
			$query['op'] = $query['advanced_search']['operator'];
894
			unset($query['advanced_search']['operator']);
895
			$query['wildcard'] = $query['advanced_search']['meth_select'];
896
			unset($query['advanced_search']['meth_select']);
897
			$original_search = $query['search'];
898
			$query['search'] = $query['advanced_search'];
899
		}
900
901
		switch ($query['template'])
902
		{
903
			case 'addressbook.index.org_rows':
904
				if ($query['order'] != 'org_name')
905
				{
906
					$query['sort'] = 'ASC';
907
					$query['order'] = 'org_name';
908
				}
909
				$query['org_view'] = $query['grouped_view'];
910
				// switch the distribution list selection off for ldap
911
				if($this->contact_repository != 'sql')
912
				{
913
					$query['no_filter2'] = true;
914
					unset($query['col_filter']['list']);	// does not work here
915
				}
916
				else
917
				{
918
					$rows = parent::organisations($query);
919
				}
920
				break;
921
			case 'addressbook.index.duplicate_rows':
922
				$query['no_filter2'] = true;			// switch the distribution list selection off
923
				unset($query['col_filter']['list']);	// does not work for duplicates
924
				$rows = parent::duplicates($query);
925
				break;
926
		}
927
928
		if ($query['advanced_search'])
929
		{
930
			$query['search'] = $original_search;
931
			unset($query['wildcard']);
932
			unset($query['op']);
933
		}
934
		$GLOBALS['egw_info']['flags']['params']['manual'] = array('page' => 'ManualAddressbookIndexOrga');
935
936
		return $rows;
937
	}
938
939
	/**
940
	 * Return the contacts in an organisation via AJAX
941
	 *
942
	 * @param string|string[] $org Organisation ID
943
	 * @param mixed $_query Query filters (category, etc) to use, or null to use session
944
	 * @return array
945
	 */
946
	public function ajax_organisation_contacts($org, $_query = null)
947
	{
948
		$org_contacts = array();
949
		$query = !$_query ? Api\Cache::getSession('addressbook', 'index') : $_query;
950
		$query['num_rows'] = -1;	// all
951
		if(!is_array($query['col_filter'])) $query['col_filter'] = array();
952
953
		if(!is_array($org)) $org = array($org);
954
		foreach($org as $org_name)
955
		{
956
			$query['grouped_view'] = $org_name;
957
			$checked = array();
958
			$readonlys = null;
959
			$this->get_rows($query,$checked,$readonlys,true);	// true = only return the id's
960
			if($checked[0])
961
			{
962
				$org_contacts = array_merge($org_contacts,$checked);
963
			}
964
		}
965
		Api\Json\Response::get()->data(array_unique($org_contacts));
966
	}
967
968
	/**
969
	 * Show the infologs of an whole organisation
970
	 *
971
	 * @param string $org
972
	 */
973
	function infolog_org_view($org)
974
	{
975
		$query = Api\Cache::getSession('addressbook', 'index');
976
		$query['num_rows'] = -1;	// all
977
		$query['grouped_view'] = $org;
978
		$query['searchletter'] = '';
979
		$checked = $readonlys = null;
980
		$this->get_rows($query,$checked,$readonlys,true);	// true = only return the id's
981
982
		if (count($checked) > 1)	// use a nicely formatted org-name as title in infolog
983
		{
984
			$parts = array();
985
			if (strpos($org,'*AND*')!== false) $org = str_replace('*AND*','&',$org);
986
			foreach(explode('|||',$org) as $part)
987
			{
988
				list(,$part) = explode(':',$part,2);
989
				if ($part) $parts[] = $part;
990
			}
991
			$org = implode(', ',$parts);
992
		}
993
		else
994
		{
995
			$org = '';	// use infolog default of link-title
996
		}
997
		Egw::redirect_link('/index.php',array(
998
			'menuaction' => 'infolog.infolog_ui.index',
999
			'action' => 'addressbook',
1000
			'action_id' => implode(',',$checked),
1001
			'action_title' => $org,
1002
		),'infolog');
1003
	}
1004
1005
	/**
1006
	 * Create or rename an existing email list
1007
	 *
1008
	 * @param int $list_id ID of existing list, or 0 for a new one
1009
	 * @param string $new_name List name
1010
	 * @param int $_owner List owner, or empty for current user
1011
	 * @param string[] [$contacts] List of contacts to add to the array
0 ignored issues
show
Documentation Bug introduced by
The doc comment [$contacts] at position 0 could not be parsed: Unknown type name '[' at position 0 in [$contacts].
Loading history...
1012
	 * @return boolean|string
1013
	 */
1014
	function ajax_set_list($list_id, $new_name, $_owner = false, $contacts = array())
1015
	{
1016
		// Set owner to current user, if not set
1017
		$owner = $_owner ? $_owner : $GLOBALS['egw_info']['user']['account_id'];
1018
		// if admin forced or set default for add_default pref
1019
		// consider default_addressbook as owner which already
1020
		// covered all cases in contacts class.
1021
		if ($owner == (int)$GLOBALS['egw']->preferences->default['addressbook']['add_default'] ||
1022
				$owner == (int)$GLOBALS['egw']->preferences->forced['addressbook']['add_default'])
1023
		{
1024
			$owner = $this->default_addressbook;
1025
		}
1026
		// Check for valid list & permissions
1027
		if(!(int)$list_id && !$this->check_list(null,EGW_ACL_ADD|EGW_ACL_EDIT,$owner))
1028
		{
1029
			Api\Json\Response::get()->apply('egw.message', array(  lang('List creation failed, no rights!'),'error'));
1030
			return;
1031
		}
1032
		if ((int)$list_id && !$this->check_list((int)$list_id, Acl::EDIT, $owner))
1033
		{
1034
			Api\Json\Response::get()->apply('egw.message', array(  lang('Insufficent rights to edit this list!'),'error'));
1035
			return;
1036
		}
1037
1038
		$list = array('list_owner' => $owner);
1039
1040
		// Rename
1041
		if($list_id)
1042
		{
1043
			$list = $this->read_list((int)$list_id);
1044
		}
1045
		$list['list_name'] = $new_name;
1046
1047
		$new_id = $this->add_list(array('list_id' => (int)$list_id), $list['list_owner'],array(),$list);
1048
1049
		if($contacts)
1050
		{
1051
			$this->add2list($contacts,$new_id);
1052
		}
1053
		Api\Json\Response::get()->apply('egw.message', array(
1054
			$new_id == $list_id ? lang('Distribution list renamed') : lang('List created'),
1055
			'success'
1056
		));
1057
		// Success, just update selectbox to new value
1058
		Api\Json\Response::get()->data($new_id == $list_id ? "true" : $new_id);
1059
	}
1060
1061
	/**
1062
	 * Ajax function to get contact data out of provided account_id
1063
	 *
1064
	 * @param string $account_id
1065
	 */
1066
	function ajax_get_contact ($account_id)
1067
	{
1068
		$bo = new Api\Contacts();
1069
		$contact = $bo->read('account:'.$account_id);
1070
		Api\Json\Response::get()->data($contact);
1071
	}
1072
1073
	/**
1074
	 * Disable / clear advanced search
1075
	 *
1076
	 * Advanced search is stored server side in session no matter what the nextmatch
1077
	 * sends, so we have to clear it here.
1078
	 */
1079
	public static function ajax_clear_advanced_search()
1080
	{
1081
		$query = Api\Cache::getSession('addressbook', 'index');
1082
		unset($query['advanced_search']);
1083
		Api\Cache::setSession('addressbook','index',$query);
1084
		Api\Cache::setSession('addressbook', 'advanced_search', false);
1085
	}
1086
1087
	/**
1088
	 * apply an action to multiple contacts
1089
	 *
1090
	 * @param string/int $action 'delete', 'vcard', 'csv' or nummerical account_id to move contacts to that addessbook
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/int at position 0 could not be parsed: Unknown type name 'string/int' at position 0 in string/int.
Loading history...
1091
	 * @param array $checked contact id's to use if !$use_all
1092
	 * @param boolean $use_all if true use all contacts of the current selection (in the session)
1093
	 * @param int &$success number of succeded actions
1094
	 * @param int &$failed number of failed actions (not enought permissions)
1095
	 * @param string &$action_msg translated verb for the actions, to be used in a message like %1 contacts 'deleted'
1096
	 * @param string/array $session_name 'index' or array with session-data depending if we are in the main list or the popup
1097
	 * @return boolean true if all actions succeded, false otherwise
1098
	 */
1099
	function action($action,$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg, $checkboxes = NULL)
1100
	{
1101
		//echo "<p>uicontacts::action('$action',".print_r($checked,true).','.(int)$use_all.",...)</p>\n";
1102
		$success = $failed = 0;
1103
		if ($use_all || in_array($action,array('remove_from_list','delete_list')))
1104
		{
1105
			// get the whole selection
1106
			$query = is_array($session_name) ? $session_name : Api\Cache::getSession('addressbook', $session_name);
1107
1108
			if ($use_all)
1109
			{
1110
				@set_time_limit(0);			// switch off the execution time limit, as it's for big selections to small
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

1110
				/** @scrutinizer ignore-unhandled */ @set_time_limit(0);			// switch off the execution time limit, as it's for big selections to small

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...
1111
				$query['num_rows'] = -1;	// all
1112
				$readonlys = null;
1113
				$this->get_rows($query,$checked,$readonlys,true);	// true = only return the id's
1114
			}
1115
		}
1116
		// replace org_name:* id's with all id's of that org
1117
		$grouped_contacts = $this->find_grouped_ids($action, $checked, $use_all, $success,$failed,$action_msg,$session_name, $msg);
1118
		if ($grouped_contacts) $checked = array_unique($checked ? array_merge($checked,$grouped_contacts) : $grouped_contacts);
1119
		//_debug_array($checked); exit;
1120
1121
		if (substr($action,0,8) == 'move_to_')
1122
		{
1123
			$action = (int)substr($action,8).(substr($action,-1) == 'p' ? 'p' : '');
1124
		}
1125
		elseif (substr($action,0,8) == 'to_list_')
1126
		{
1127
			$to_list = (int)substr($action,8);
1128
			$action = 'to_list';
1129
		}
1130
		elseif (substr($action,0,9) == 'document_')
1131
		{
1132
			$document = substr($action,9);
1133
			$action = 'document';
1134
		}
1135
		elseif(substr($action,0,4) == 'cat_')	// cat_add_123 or cat_del_456
1136
		{
1137
			$cat_id = (int)substr($action, 8);
1138
			$action = substr($action,0,7);
1139
		}
1140
		// Security: stop non-admins to export more then the configured number of contacts
1141
		if (in_array($action,array('csv','vcard')) && $this->config['contact_export_limit'] && !Api\Storage\Merge::is_export_limit_excepted() &&
1142
			(!is_numeric($this->config['contact_export_limit']) || count($checked) > $this->config['contact_export_limit']))
1143
		{
1144
			$action_msg = lang('exported');
1145
			$failed = count($checked);
1146
			return false;
1147
		}
1148
		switch($action)
1149
		{
1150
			case 'vcard':
1151
				$action_msg = lang('exported');
1152
				$vcard = new addressbook_vcal('addressbook','text/vcard');
1153
				$vcard->export($checked);
1154
				// does not return!
1155
				$Ok = false;
1156
				break;
1157
1158
			case 'merge':
1159
				$error_msg = null;
1160
				$success = $this->merge($checked,$error_msg);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\Contacts::merge() has too many arguments starting with $error_msg. ( Ignorable by Annotation )

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

1160
				/** @scrutinizer ignore-call */ 
1161
    $success = $this->merge($checked,$error_msg);

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...
1161
				$failed = count($checked) - (int)$success;
1162
				$action_msg = lang('merged');
1163
				$checked = array();	// to not start the single actions
1164
				break;
1165
1166
			case 'delete_list':
1167
				if (!$query['filter2'])
1168
				{
1169
					$msg = lang('You need to select a distribution list');
1170
				}
1171
				elseif($this->delete_list($query['filter2']) === false)
0 ignored issues
show
introduced by
The condition $this->delete_list($query['filter2']) === false is always false.
Loading history...
1172
				{
1173
					$msg = lang('Insufficent rights to delete this list!');
1174
				}
1175
				else
1176
				{
1177
					$msg = lang('Distribution list deleted');
1178
					unset($query['filter2']);
1179
					Api\Cache::setSession('addressbook', $session_name, $query);
1180
				}
1181
				return false;
1182
1183
			case 'document':
1184
				if (!$document) $document = $this->prefs['default_document'];
1185
				$document_merge = new Api\Contacts\Merge();
1186
				$msg = $document_merge->download($document, $checked, '', $this->prefs['document_dir']);
1187
				$failed = count($checked);
1188
				return false;
1189
1190
			case 'infolog_add':
1191
				Framework::popup(Egw::link('/index.php',array(
1192
						'menuaction' => 'infolog.infolog_ui.edit',
1193
						'type' => 'task',
1194
						'action' => 'addressbook',
1195
						'action_id' => implode(',',$checked),
1196
					)),'_blank',Link::get_registry('infolog', 'add_popup'));
1197
				$msg = '';	// no message, as we send none in javascript too and users sees opening popup
1198
				return false;
1199
1200
			case 'calendar_add':	// add appointment for org-views, other views are handled directly in javascript
1201
				Framework::popup(Egw::link('/index.php',array(
1202
						'menuaction' => 'calendar.calendar_uiforms.edit',
1203
						'participants' => 'c'.implode(',c',$checked),
1204
					)),'_blank',Link::get_registry('calendar', 'add_popup'));
1205
				$msg = '';	// no message, as we send none in javascript too and users sees opening popup
1206
				return false;
1207
1208
			case 'calendar_view':	// show calendar for org-views, although all views are handled directly in javascript
1209
				Egw::redirect_link('/index.php',array(
1210
					'menuaction' => 'calendar.calendar_uiviews.index',
1211
					'owner' => 'c'.implode(',c',$checked),
1212
				));
1213
		}
1214
		foreach($checked as $id)
1215
		{
1216
			switch($action)
1217
			{
1218
				case 'cat_add':
1219
				case 'cat_del':
1220
					if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::EDIT,$contact)))
1221
					{
1222
						$action_msg = $action == 'cat_add' ? lang('categorie added') : lang('categorie delete');
1223
						$cat_ids = $contact['cat_id'] ? explode(',', $contact['cat_id']) : array();   //existing Api\Categories
1224
						if ($action == 'cat_add')
1225
						{
1226
							$cat_ids[] = $cat_id;
1227
							$cat_ids = array_unique($cat_ids);
1228
						}
1229
						elseif ((($key = array_search($cat_id,$cat_ids))) !== false)
1230
						{
1231
							unset($cat_ids[$key]);
1232
						}
1233
						$ids = $cat_ids ? implode(',',$cat_ids) : null;
1234
						if ($ids !== $contact['cat_id'])
1235
						{
1236
							$contact['cat_id'] = $ids;
1237
							$Ok = $this->save($contact);
1238
						}
1239
					}
1240
					break;
1241
1242
				case 'delete':
1243
					$action_msg = lang('deleted');
1244
					if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::DELETE,$contact)))
1245
					{
1246
						if ($contact['owner'] ||	// regular contact or
1247
							empty($contact['account_id']) ||	// accounts without account_id
1248
							// already deleted account (should no longer happen, but needed to allow for cleanup)
1249
							$contact['tid'] == self::DELETED_TYPE)
1250
						{
1251
							$Ok = $this->delete($id, $contact['tid'] != self::DELETED_TYPE && $contact['account_id']);
1252
						}
1253
						// delete single account --> redirect to admin
1254
						elseif (count($checked) == 1 && $contact['account_id'])
1255
						{
1256
							Egw::redirect_link('/index.php',array(
1257
								'menuaction' => 'admin.admin_account.delete',
1258
								'account_id' => $contact['account_id'],
1259
							));
1260
							// this does NOT return!
1261
						}
1262
						else	// no mass delete of Api\Accounts
1263
						{
1264
							$Ok = false;
1265
						}
1266
					}
1267
					break;
1268
1269
				case 'undelete':
1270
					$action_msg = lang('recovered');
1271
					if (($contact = $this->read($id)))
1272
					{
1273
						$contact['tid'] = 'n';
1274
						$Ok = $this->save($contact);
1275
					}
1276
					break;
1277
1278
				case 'email':
1279
				case 'email_home':
1280
					/* this cant work anymore, as Framework::set_onload does not longer exist
1281
					$action_fallback = $action == 'email' ? 'email_home' : 'email';
1282
					$action_msg = lang('added');
1283
					if(($contact = $this->read($id)))
1284
					{
1285
						if(strpos($contact[$action],'@') !== false)
1286
						{
1287
							$email = $contact[$action];
1288
						}
1289
						elseif(strpos($contact[$action_fallback],'@') !== false)
1290
						{
1291
							$email = $contact[$action_fallback];
1292
						}
1293
						else
1294
						{
1295
							$Ok = $email = false;
1296
						}
1297
						if($email)
1298
						{
1299
							$contact['n_fn'] = str_replace(array(',','@'),' ',$contact['n_fn']);
1300
							Framework::set_onload("addEmail('".addslashes(
1301
								$contact['n_fn'] ? $contact['n_fn'].' <'.trim($email).'>' : trim($email))."');");
1302
							//error_log(__METHOD__.__LINE__."addEmail('".addslashes(
1303
							//	$contact['n_fn'] ? $contact['n_fn'].' <'.trim($email).'>' : trim($email))."');");
1304
							$Ok = true;
1305
						}
1306
					}*/
1307
					break;
1308
1309
				case 'remove_from_list':
1310
					$action_msg = lang('removed from distribution list');
1311
					if (!$query['filter2'])
1312
					{
1313
						$msg = lang('You need to select a distribution list');
1314
						return false;
1315
					}
1316
					else
1317
					{
1318
						$Ok = $this->remove_from_list($id,$query['filter2']) !== false;
1319
					}
1320
					break;
1321
1322
				case 'to_list':
1323
					$action_msg = lang('added to distribution list');
1324
					if (!$to_list)
1325
					{
1326
						$msg = lang('You need to select a distribution list');
1327
						return false;
1328
					}
1329
					else
1330
					{
1331
						$Ok = $this->add2list($id,$to_list) !== false;
1332
					}
1333
					break;
1334
1335
				default:	// move to an other addressbook
1336
					if (!(int)$action || !($this->grants[(string) (int) $action] & Acl::EDIT))	// might be ADD in the future
1337
					{
1338
						return false;
1339
					}
1340
					if (!$checkboxes['move_to_copy'])
1341
					{
1342
						$action_msg = lang('moved');
1343
						if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::DELETE,$contact)))
1344
						{
1345
							if (!$contact['owner'])		// no (mass-)move of Api\Accounts
1346
							{
1347
								$Ok = false;
1348
							}
1349
							elseif ($contact['owner'] != (int)$action || $contact['private'] != (int)(substr($action,-1) == 'p'))
1350
							{
1351
								$contact['owner'] = (int) $action;
1352
								$contact['private'] = (int)(substr($action,-1) == 'p');
1353
								$Ok = $this->save($contact);
1354
							}
1355
						}
1356
					}
1357
					else
1358
					{
1359
						$action_msg = lang('copied');
1360
						if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::READ,$contact)))
1361
						{
1362
							if ($contact['owner'] != (int)$action || $contact['private'] != (int)(substr($action,-1) == 'p'))
1363
							{
1364
								$this->copy_contact($contact, false);	// do NOT use self::$copy_fields, copy everything but uid etc.
1365
								$links = $contact['link_to']['to_id'];
1366
								$contact['owner'] = (int) $action;
1367
								$contact['private'] = (int)(substr($action,-1) == 'p');
1368
								$Ok = $this->save($contact);
1369
								if ($Ok && is_array($links))
1370
								{
1371
									Link::link('addressbook', $contact['id'], $links);
1372
								}
1373
							}
1374
						}
1375
					}
1376
					break;
1377
			}
1378
			if ($Ok)
1379
			{
1380
				++$success;
1381
			}
1382
			elseif ($action != 'email' && $action != 'email_home')
1383
			{
1384
				++$failed;
1385
			}
1386
		}
1387
		return !$failed;
1388
	}
1389
1390
	/**
1391
	 * Find the individual contact IDs for a list of grouped contacts
1392
	 *
1393
	 * Successful lookups are removed from the checked array.
1394
	 *
1395
	 * Used for action on organisation and duplicate views
1396
	 * @param string/int $action 'delete', 'vcard', 'csv' or nummerical account_id to move contacts to that addessbook
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/int at position 0 could not be parsed: Unknown type name 'string/int' at position 0 in string/int.
Loading history...
1397
	 * @param array $checked contact id's to use if !$use_all
1398
	 * @param boolean $use_all if true use all contacts of the current selection in the session (NOT used!)
1399
	 * @param int &$success number of succeded actions
1400
	 * @param int &$failed number of failed actions (not enought permissions)
1401
	 * @param string &$action_msg translated verb for the actions, to be used in a message like %1 contacts 'deleted'
1402
	 * @param string/array $session_name 'index' or 'email', or array with session-data depending if we are in the main list or the popup
1403
	 *
1404
	 * @return array List of contact IDs in the provided groups
1405
	 */
1406
	protected function find_grouped_ids($action,&$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg)
1407
	{
1408
		unset($use_all);
1409
		$grouped_contacts = array();
1410
		foreach((array)$checked as $n => $id)
1411
		{
1412
			if (substr($id,0,9) == 'org_name:' || substr($id, 0,10) == 'duplicate:')
1413
			{
1414
				if (count($checked) == 1 && !count($grouped_contacts) && $action == 'infolog')
1415
				{
1416
					return $this->infolog_org_view($id);	// uses the org-name, instead of 'selected contacts'
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->infolog_org_view($id) targeting addressbook_ui::infolog_org_view() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1417
				}
1418
				unset($checked[$n]);
1419
				$query = Api\Cache::getSession('addressbook', $session_name);
1420
				$query['num_rows'] = -1;	// all
1421
				$query['grouped_view'] = $id;
1422
				unset($query['filter2']);
1423
				$extra = $readonlys = null;
1424
				$this->get_rows($query,$extra,$readonlys,true);	// true = only return the id's
1425
1426
				// Merge them here, so we only merge the ones that are duplicates,
1427
				// not merge all selected together
1428
				if($action == 'merge_duplicates')
1429
				{
1430
					$loop_success = $loop_fail = 0;
1431
					$this->action('merge', $extra, false, $loop_success, $loop_fail, $action_msg,$session_name,$msg);
1432
					$success += $loop_success;
1433
					$failed += $loop_fail;
1434
				}
1435
				if ($extra[0])
1436
				{
1437
					$grouped_contacts = array_merge($grouped_contacts,$extra);
1438
				}
1439
			}
1440
		}
1441
		return $grouped_contacts;
1442
	}
1443
1444
	/**
1445
	 * Copy a given contact (not storing it!)
1446
	 *
1447
	 * Taken care only configured fields get copied and certain fields never to copy (uid etc.).
1448
	 *
1449
	 * @param array& $content
1450
	 * @param boolean $only_copy_fields =true true: only copy fields configured for copying (eg. no name),
1451
	 *		false: copy everything, but never to copy fields
1452
	 */
1453
	function copy_contact(array &$content, $only_copy_fields=true)
1454
	{
1455
		$content['link_to']['to_id'] = 0;
1456
		Link::link('addressbook',$content['link_to']['to_id'],'addressbook',$content['id'],
1457
			lang('Copied by %1, from record #%2.',Api\Accounts::format_username('',
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with EGroupware\Api\Accounts:...']['account_lastname']). ( Ignorable by Annotation )

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

1457
			/** @scrutinizer ignore-call */ 
1458
   lang('Copied by %1, from record #%2.',Api\Accounts::format_username('',

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...
1458
			$GLOBALS['egw_info']['user']['account_firstname'],$GLOBALS['egw_info']['user']['account_lastname']),
1459
			$content['id']));
1460
		// create a new contact with the content of the old
1461
		foreach(array_keys($content) as $key)
1462
		{
1463
			if($only_copy_fields && !in_array($key, self::$copy_fields) || in_array($key, array('id','etag','carddav_name','uid')))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($only_copy_fields && ! ...'carddav_name', 'uid')), Probably Intended Meaning: $only_copy_fields && (! ...carddav_name', 'uid')))
Loading history...
1464
			{
1465
				unset($content[$key]);
1466
			}
1467
		}
1468
		if(!isset($content['owner']))
1469
		{
1470
			$content['owner'] = $this->default_private ? $this->user.'p' : $this->default_addressbook;
1471
		}
1472
		$content['creator'] = $this->user;
1473
		$content['created'] = $this->now_su;
1474
	}
1475
1476
	/**
1477
	 * rows callback for index nextmatch
1478
	 *
1479
	 * @internal
1480
	 * @param array &$query
1481
	 * @param array &$rows returned rows/cups
1482
	 * @param array &$readonlys eg. to disable buttons based on Acl
1483
	 * @param boolean $id_only =false if true only return (via $rows) an array of contact-ids, dont save state to session
1484
	 * @return int total number of contacts matching the selection
1485
	 */
1486
	function get_rows(&$query,&$rows,&$readonlys,$id_only=false)
1487
	{
1488
		$what = $query['sitemgr_display'] ? $query['sitemgr_display'] : 'index';
1489
1490
		if (!$id_only && !$query['csv_export'])	// do NOT store state for csv_export or querying id's (no regular view)
1491
		{
1492
			$store_query = $query;
1493
			// Do not store these
1494
			foreach(array('options-cat_id','actions','action_links','placeholder_actions') as $key)
1495
			{
1496
				unset($store_query[$key]);
1497
			}
1498
			$old_state = $store_query;
1499
			Api\Cache::setSession('addressbook', $what, $store_query);
1500
		}
1501
		else
1502
		{
1503
			$old_state = Api\Cache::getSession('addressbook', $what);
1504
		}
1505
		$GLOBALS['egw']->session->commit_session();
1506
		if (!isset($this->grouped_views[(string) $query['grouped_view']]) || strpos($query['grouped_view'],':') === false)
1507
		{
1508
			// we don't have a grouped view, unset the according col_filters
1509
			$this->unset_grouped_filters($query);
1510
		}
1511
1512
		if (isset($this->grouped_views[(string) $query['grouped_view']]))
1513
		{
1514
			// we have a grouped view, reset the advanced search
1515
			if(!$query['search'] && $old_state['advanced_search']) $query['advanced_search'] = $old_state['advanced_search'];
1516
		}
1517
		elseif(!$query['search'] && array_key_exists('advanced_search',$old_state))	// eg. paging in an advanced search
1518
		{
1519
			$query['advanced_search'] = $old_state['advanced_search'];
1520
		}
1521
1522
		// Make sure old lettersearch filter doesn't stay - current letter filter will be added later
1523
		foreach($query['col_filter'] as $key => $col_filter)
1524
		{
1525
			if(!is_numeric($key)) continue;
1526
			if(preg_match('/'.$GLOBALS['egw']->db->capabilities['case_insensitive_like'].
1527
				' '.$GLOBALS['egw']->db->quote('[a-z]%').'$/i',$col_filter) == 1
1528
			)
1529
			{
1530
				unset($query['col_filter'][$key]);
1531
			}
1532
		}
1533
1534
		//echo "<p>uicontacts::get_rows(".print_r($query,true).")</p>\n";
1535
		if (!$id_only)
1536
		{
1537
			// check if accounts are stored in ldap, which does NOT yet support the org-views
1538
			if ($this->so_accounts && $query['filter'] === '0' && $query['grouped_view'])
1539
			{
1540
				if ($old_state['filter'] === '0')	// user changed to org_view
1541
				{
1542
					$query['filter'] = '';			// --> change filter to all contacts
1543
				}
1544
				else								// user changed to accounts
1545
				{
1546
					$query['grouped_view'] = '';		// --> change to regular contacts view
1547
				}
1548
			}
1549
			if ($query['grouped_view'] && isset($this->grouped_views[$old_state['grouped_view']]) && !isset($this->grouped_views[$query['grouped_view']]))
1550
			{
1551
				$query['searchletter'] = '';		// reset lettersearch if viewing the contacts of one group (org or duplicates)
1552
			}
1553
			// save the state of the index in the user prefs
1554
			$state = serialize(array(
1555
				'filter'        => $query['filter'],
1556
				'cat_id'        => $query['cat_id'],
1557
				'order'         => $query['order'],
1558
				'sort'		    => $query['sort'],
1559
				'col_filter'    => array('tid' => $query['col_filter']['tid']),
1560
				'grouped_view'  => $query['grouped_view'],
1561
			));
1562
			if ($state != $this->prefs[$what.'_state'] && !$query['csv_export'])
1563
			{
1564
				$GLOBALS['egw']->preferences->add('addressbook',$what.'_state',$state);
1565
				// save prefs, but do NOT invalid the cache (unnecessary)
1566
				$GLOBALS['egw']->preferences->save_repository(false,'user',false);
1567
			}
1568
		}
1569
		unset($old_state);
1570
1571
		if ((string)$query['cat_id'] != '')
1572
		{
1573
			$query['col_filter']['cat_id'] = $query['cat_id'] ? $query['cat_id'] : null;
1574
		}
1575
		else
1576
		{
1577
			unset($query['col_filter']['cat_id']);
1578
		}
1579
		if ($query['filter'] !== '')	// not all addressbooks
1580
		{
1581
			$query['col_filter']['owner'] = (string) (int) $query['filter'];
1582
1583
			if ($this->private_addressbook)
1584
			{
1585
				$query['col_filter']['private'] = substr($query['filter'],-1) == 'p' ? 1 : 0;
1586
			}
1587
		}
1588
		else
1589
		{
1590
			unset($query['col_filter']['owner']);
1591
			unset($query['col_filter']['private']);
1592
		}
1593
		if ((int)$query['filter2'])	// not no distribution list
1594
		{
1595
			$query['col_filter']['list'] = (string) (int) $query['filter2'];
1596
		}
1597
		else
1598
		{
1599
			unset($query['col_filter']['list']);
1600
		}
1601
		if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '1')
1602
		{
1603
			$query['col_filter']['account_id'] = null;
1604
		}
1605
		else
1606
		{
1607
			unset($query['col_filter']['account_id']);
1608
		}
1609
1610
		// all backends allow now at least to use groups as distribution lists
1611
		$query['no_filter2'] = false;
1612
1613
		// Grouped view
1614
		if (isset($this->grouped_views[(string) $query['grouped_view']]) && !$query['col_filter']['parent_id'])
1615
		{
1616
			$query['grouped_view_label'] = '';
1617
			$rows = $this->get_grouped_rows($query);
1618
		}
1619
		else	// contacts view
1620
		{
1621
			if ($query['sitemgr_display'])
1622
			{
1623
				$query['template'] = $query['sitemgr_display'].'.rows';
1624
			}
1625
			else
1626
			{
1627
				$query['template'] = 'addressbook.index.rows';
1628
			}
1629
			if($query['col_filter']['parent_id'])
1630
			{
1631
				$query['grouped_view'] = $query['col_filter']['parent_id'];
1632
				$query['template'] = strpos($query['grouped_view'], 'duplicate') === 0 ?
1633
						'addressbook.index.duplicate_rows' : 'addressbook.index.org_rows';
1634
			}
1635
			// Query doesn't like parent_id
1636
			unset($query['col_filter']['parent_id']);
1637
			if ($query['grouped_view'])	// view the contacts of one organisation only
1638
			{
1639
				if (strpos($query['grouped_view'],'*AND*') !== false) $query['grouped_view'] = str_replace('*AND*','&',$query['grouped_view']);
1640
				$fields = explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']);
1641
				foreach(explode('|||',$query['grouped_view']) as $part)
1642
				{
1643
					list($name,$value) = explode(':',$part,2);
1644
					// do NOT set invalid column, as this gives an SQL error ("AND AND" in sql)
1645
					if (static::$duplicate_fields[$name] && $value && (
1646
							strpos($query['grouped_view'], 'duplicate:') === 0 && in_array($name, $fields) ||
1647
							strpos($query['grouped_view'], 'duplicate:') !== 0
1648
					))
1649
					{
1650
						$query['col_filter'][$name] = $value;
1651
					}
1652
				}
1653
			}
1654
			else if($query['actions'] && !$query['actions']['edit'])
1655
			{
1656
				// Just switched from grouped view, update actions
1657
				$query['actions'] = $this->get_actions($query['col_filter']['tid']);
1658
			}
1659
			// translate the select order to the really used over all 3 columns
1660
			$sort = $query['sort'];
1661
			switch($query['order'])		// "xxx<>'' DESC" sorts contacts with empty order-criteria always at the end
1662
			{							// we don't exclude them, as the total would otherwise depend on the order-criteria
1663
				case 'org_name':
1664
					$order = "egw_addressbook.org_name<>''DESC,egw_addressbook.org_name $sort,n_family $sort,n_given $sort";
1665
					break;
1666
				default:
1667
					if ($query['order'][0] == '#')	// we order by a custom field
1668
					{
1669
						$order = "{$query['order']} $sort,org_name $sort,n_family $sort,n_given $sort";
1670
						break;
1671
					}
1672
					$query['order'] = 'n_family';
1673
				case 'n_family':
1674
					$order = "n_family<>'' DESC,n_family $sort,n_given $sort,org_name $sort";
1675
					break;
1676
				case 'n_given':
1677
					$order = "n_given<>'' DESC,n_given $sort,n_family $sort,org_name $sort";
1678
					break;
1679
				case 'n_fileas':
1680
					$order = "n_fileas<>'' DESC,n_fileas $sort";
1681
					break;
1682
				case 'adr_one_postalcode':
1683
				case 'adr_two_postalcode':
1684
					$order = $query['order']."<>'' DESC,".$query['order']." $sort,org_name $sort,n_family $sort,n_given $sort";
1685
					break;
1686
				case 'contact_modified':
1687
				case 'contact_created':
1688
					$order = "$query[order] IS NULL,$query[order] $sort,org_name $sort,n_family $sort,n_given $sort";
1689
					break;
1690
				case 'contact_id':
1691
					$order = "egw_addressbook.$query[order] $sort";
1692
			}
1693
			if ($query['searchletter'])	// only show contacts if the order-criteria starts with the given letter
1694
			{
1695
				$no_letter_search = array('adr_one_postalcode', 'adr_two_postalcode', 'contact_id', 'contact_created','contact_modified');
1696
				$query['col_filter'][] = (in_array($query['order'],$no_letter_search) ? 'org_name' : (substr($query['order'],0,1)=='#'?'':'egw_addressbook.').$query['order']).' '.
1697
					$GLOBALS['egw']->db->capabilities['case_insensitive_like'].' '.$GLOBALS['egw']->db->quote($query['searchletter'].'%');
1698
			}
1699
			$wildcard = '%';
1700
			$op = 'OR';
1701
			if ($query['advanced_search'])
1702
			{
1703
				// Make sure op & wildcard are only valid options
1704
				$op = $query['advanced_search']['operator'] == $op ? $op : 'AND';
1705
				unset($query['advanced_search']['operator']);
1706
				$wildcard = $query['advanced_search']['meth_select'] == $wildcard ? $wildcard : '';
1707
				unset($query['advanced_search']['meth_select']);
1708
			}
1709
1710
			$rows = parent::search($query['advanced_search'] ? $query['advanced_search'] : $query['search'],$id_only,
1711
				$order,'',$wildcard,false,$op,array((int)$query['start'],(int) $query['num_rows']),$query['col_filter']);
1712
1713
			// do we need to read the custom fields, depends on the column is enabled and customfields
1714
			$columsel = $this->prefs['nextmatch-addressbook.index.rows'];
1715
			$available_distib_lists=$this->get_lists(Acl::READ);
1716
			$columselection = $columsel ? explode(',',$columsel) : array();
1717
			$ids = $calendar_participants = array();
1718
			if (!$id_only && $rows)
0 ignored issues
show
Bug Best Practice introduced by
The expression $rows 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...
1719
			{
1720
				$show_custom_fields = (in_array('customfields',$columselection) || $this->config['index_load_cfs']) && $this->customfields;
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
introduced by
Consider adding parentheses for clarity. Current Interpretation: $show_custom_fields = (i...&& $this->customfields), Probably Intended Meaning: $show_custom_fields = in...&& $this->customfields)
Loading history...
1721
				$show_calendar = $this->config['disable_event_column'] != 'True' && in_array('calendar_calendar',$columselection);
1722
				$show_distributionlist = in_array('distrib_lists',$columselection) || count($available_distib_lists);
1723
				if ($show_calendar || $show_custom_fields || $show_distributionlist)
1724
				{
1725
					foreach($rows as $val)
1726
					{
1727
						$ids[] = $val['id'];
1728
						$calendar_participants[$val['id']] = $val['account_id'] ? $val['account_id'] : 'c'.$val['id'];
1729
					}
1730
					if ($show_custom_fields)
1731
					{
1732
						$selected_cfs = array();
1733
						if(in_array('customfields',$columselection))
1734
						{
1735
							foreach($columselection as $col)
1736
							{
1737
								if ($col[0] == '#') $selected_cfs[] = substr($col,1);
1738
							}
1739
						}
1740
						$selected_cfs = array_unique(array_merge($selected_cfs, (array)$this->config['index_load_cfs']));
1741
						$customfields = $this->read_customfields($ids,$selected_cfs);
1742
					}
1743
					if ($show_calendar && !empty($ids)) $calendar = $this->read_calendar($calendar_participants);
1744
					// distributionlist memership for the entrys
1745
					//_debug_array($this->get_lists(Acl::EDIT));
1746
					if ($show_distributionlist && $available_distib_lists)
1747
					{
1748
						$distributionlist = $this->read_distributionlist($ids,array_keys($available_distib_lists));
1749
					}
1750
				}
1751
			}
1752
		}
1753
		if (!$rows) $rows = array();
0 ignored issues
show
Bug Best Practice introduced by
The expression $rows 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...
1754
1755
		if ($id_only)
1756
		{
1757
			foreach($rows as $n => $row)
1758
			{
1759
				$rows[$n] = $row['id'];
1760
			}
1761
			return $this->total;	// no need to set other fields or $readonlys
1762
		}
1763
		$order = $query['order'];
1764
1765
		$readonlys = array();
1766
		foreach($rows as $n => &$row)
1767
		{
1768
			$given = $row['n_given'] ? $row['n_given'] : ($row['n_prefix'] ? $row['n_prefix'] : '');
1769
1770
			switch($order)
1771
			{
1772
				default:	// postalcode, created, modified, ...
1773
				case 'org_name':
1774
					$row['line1'] = $row['org_name'];
1775
					$row['line2'] = $row['n_family'].($given ? ', '.$given : '');
1776
					break;
1777
				case 'n_family':
1778
					$row['line1'] = $row['n_family'].($given ? ', '.$given : '');
1779
					$row['line2'] = $row['org_name'];
1780
					break;
1781
				case 'n_given':
1782
					$row['line1'] = $given.' '.$row['n_family'];
1783
					$row['line2'] = $row['org_name'];
1784
					break;
1785
				case 'n_fileas':
1786
					if (!$row['n_fileas']) $row['n_fileas'] = $this->fileas($row);
1787
					list($row['line1'],$row['line2']) = explode(': ',$row['n_fileas']);
1788
					break;
1789
			}
1790
			if (isset($this->grouped_views[(string) $query['grouped_view']]))
1791
			{
1792
				$row['type'] = 'home';
1793
				$row['type_label'] = $query['grouped_view'] == 'duplicate' ? lang('Duplicates') : lang('Organisation');
1794
1795
				if ($query['filter'] && !($this->grants[(int)$query['filter']] & Acl::DELETE))
1796
				{
1797
					$row['class'] .= 'rowNoDelete ';
1798
				}
1799
				$row['class'] .= 'rowNoEdit ';	// no edit in OrgView
1800
				$row['class'] .= $query['grouped_view'] == 'duplicates' ? 'contact_duplicate' : 'contact_organisation ';
1801
			}
1802
			else
1803
			{
1804
				$this->type_icon($row['owner'],$row['private'],$row['tid'],$row['type'],$row['type_label']);
1805
1806
				static $tel2show = array('tel_work','tel_cell','tel_home','tel_fax');
1807
				static $prefer_marker = null;
1808
				if (is_null($prefer_marker))
1809
				{
1810
					// as et2 adds options with .text(), it can't be entities, but php knows no string literals with utf-8
1811
					$prefer_marker = html_entity_decode(' &#9734;', ENT_NOQUOTES, 'utf-8');
1812
				}
1813
				foreach($tel2show as $name)
1814
				{
1815
					$row[$name] .= ' '.($row['tel_prefer'] == $name ? $prefer_marker : '');		// .' ' to NOT remove the field
1816
				}
1817
				// allways show the prefered phone, if not already shown
1818
				if (!in_array($row['tel_prefer'],$tel2show) && $row[$row['tel_prefer']])
1819
				{
1820
					$row['tel_prefered'] = $row[$row['tel_prefer']].$prefer_marker;
1821
				}
1822
				// Show nice name as status text
1823
				if($row['tel_prefer'])
1824
				{
1825
					$row['tel_prefer_label'] = $this->contact_fields[$row['tel_prefer']];
1826
				}
1827
				if (!$row['owner'] && $row['account_id'] > 0)
1828
				{
1829
					$row['class'] .= 'rowAccount rowNoDelete ';
1830
				}
1831
				elseif (!$this->check_perms(Acl::DELETE,$row) || (!$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge' && $query['col_filter']['tid'] == self::DELETED_TYPE))
1832
				{
1833
					$row['class'] .= 'rowNoDelete ';
1834
				}
1835
				if (!$this->check_perms(Acl::EDIT,$row))
1836
				{
1837
					$row['class'] .= 'rowNoEdit ';
1838
				}
1839
				$row['class'] .= 'contact_contact ';
1840
1841
				unset($row['jpegphoto']);	// unused and messes up json encoding (not utf-8)
1842
1843
				if (isset($customfields[$row['id']]))
1844
				{
1845
					foreach($this->customfields as $name => $data)
1846
					{
1847
						$row['#'.$name] = $customfields[$row['id']]['#'.$name];
1848
					}
1849
				}
1850
				if (isset($distributionlist[$row['id']]))
1851
				{
1852
					$row['distrib_lists'] = implode("\n",array_values($distributionlist[$row['id']]));
1853
					//if ($show_distributionlist) $readonlys['distrib_lists'] =true;
1854
				}
1855
				if (isset($calendar[$calendar_participants[$row['id']]]))
1856
				{
1857
					foreach($calendar[$calendar_participants[$row['id']]] as $name => $data)
1858
					{
1859
						$row[$name] = $data;
1860
					}
1861
				}
1862
			}
1863
1864
			// hide region for address format 'postcode_city'
1865
			if (($row['addr_format']  = $this->addr_format_by_country($row['adr_one_countryname']))=='postcode_city') unset($row['adr_one_region']);
1866
			if (($row['addr_format2'] = $this->addr_format_by_country($row['adr_two_countryname']))=='postcode_city') unset($row['adr_two_region']);
1867
1868
			// respect category permissions
1869
			if(!empty($row['cat_id']))
1870
			{
1871
				$row['cat_id'] = $this->categories->check_list(Acl::READ,$row['cat_id']);
1872
			}
1873
		}
1874
		$rows['no_distribution_list'] = (bool)$query['filter2'];
1875
1876
		// disable customfields column, if we have no customefield(s)
1877
		if (!$this->customfields)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
1878
		{
1879
			$rows['no_customfields'] = true;
1880
		}
1881
1882
		// Disable next/last date if so configured
1883
		if($this->config['disable_event_column'] == 'True')
1884
		{
1885
			$rows['no_event_column'] = true;
1886
		}
1887
1888
		$rows['order'] = $order;
1889
		$rows['call_popup'] = $this->config['call_popup'];
1890
		$rows['customfields'] = array_values($this->customfields);
1891
1892
		// full app-header with all search criteria specially for the print
1893
		$header = array();
1894
		if ($query['filter'] !== '' && !isset($this->grouped_views[$query['grouped_view']]))
1895
		{
1896
			$header[] = ($query['filter'] == '0' ? lang('accounts') :
1897
				($GLOBALS['egw']->accounts->get_type($query['filter']) == 'g' ?
1898
					lang('Group %1',$GLOBALS['egw']->accounts->id2name($query['filter'])) :
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $GLOBALS['egw']->account...2name($query['filter']). ( Ignorable by Annotation )

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

1898
					/** @scrutinizer ignore-call */ 
1899
     lang('Group %1',$GLOBALS['egw']->accounts->id2name($query['filter'])) :

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...
1899
					Api\Accounts::username((int)$query['filter']).
1900
						(substr($query['filter'],-1) == 'p' ? ' ('.lang('private').')' : '')));
1901
		}
1902
		if ($query['grouped_view'])
1903
		{
1904
			$header[] = $query['grouped_view_label'];
1905
			// Make sure option is there
1906
			if(!array_key_exists($query['grouped_view'], $this->grouped_views))
1907
			{
1908
				$this->grouped_views += $this->_get_grouped_name($query['grouped_view']);
1909
				$rows['sel_options']['grouped_view'] = $this->grouped_views;
1910
			}
1911
		}
1912
		if($query['advanced_search'])
1913
		{
1914
			$header[] = lang('Advanced search');
1915
		}
1916
		if ($query['cat_id'])
1917
		{
1918
			$header[] = lang('Category').' '.$GLOBALS['egw']->categories->id2name($query['cat_id']);
1919
		}
1920
		if ($query['searchletter'])
1921
		{
1922
			$order = $order == 'n_given' ? lang('first name') : ($order == 'n_family' ? lang('last name') : lang('Organisation'));
1923
			$header[] = lang("%1 starts with '%2'",$order,$query['searchletter']);
1924
		}
1925
		if ($query['search'] && !$query['advanced_search']) // do not add that, if we have advanced search active
1926
		{
1927
			$header[] = lang("Search for '%1'",$query['search']);
1928
		}
1929
		$GLOBALS['egw_info']['flags']['app_header'] = implode(': ', $header);
1930
1931
		return $this->total;
1932
	}
1933
1934
	/**
1935
	 * Get addressbook type icon from owner, private and tid
1936
	 *
1937
	 * @param int $owner user- or group-id or 0 for Api\Accounts
1938
	 * @param boolean $private
1939
	 * @param string $tid 'n' for regular addressbook
1940
	 * @param string &$icon icon-name
1941
	 * @param string &$label translated label
1942
	 */
1943
	function type_icon($owner,$private,$tid,&$icon,&$label)
1944
	{
1945
		if (!$owner)
1946
		{
1947
			$icon = 'accounts';
1948
			$label = lang('accounts');
1949
		}
1950
		elseif ($private)
1951
		{
1952
			$icon = 'private';
1953
			$label = lang('private');
1954
		}
1955
		elseif ($GLOBALS['egw']->accounts->get_type($owner) == 'g')
1956
		{
1957
			$icon = 'group';
1958
			$label = lang('group %1',$GLOBALS['egw']->accounts->id2name($owner));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $GLOBALS['egw']->accounts->id2name($owner). ( Ignorable by Annotation )

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

1958
			$label = /** @scrutinizer ignore-call */ lang('group %1',$GLOBALS['egw']->accounts->id2name($owner));

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...
1959
		}
1960
		else
1961
		{
1962
			$icon = 'personal';
1963
			$label = $owner == $this->user ? lang('personal') : Api\Accounts::username($owner);
1964
		}
1965
		// show tid icon for tid!='n' AND only if one is defined
1966
		if ($tid != 'n' && Api\Image::find('addressbook',$this->content_types[$tid]['name']))
1967
		{
1968
			$icon = Api\Image::find('addressbook',$this->content_types[$tid]['name']);
1969
		}
1970
1971
		// Legacy - from when icons could be anywhere
1972
		if ($tid != 'n' && $this->content_types[$tid]['options']['icon'])
1973
		{
1974
			$icon = $this->content_types[$tid]['options']['icon'];
1975
			$label = $this->content_types[$tid]['name'].' ('.$label.')';
1976
		}
1977
	}
1978
1979
	/**
1980
	* Edit a contact
1981
	*
1982
	* @param array $content=null submitted content
1983
	* @param int $_GET['contact_id'] contact_id mainly for popup use
1984
	* @param bool $_GET['makecp'] true if you want to copy the contact given by $_GET['contact_id']
1985
	*/
1986
	function edit($content=null)
1987
	{
1988
		if (is_array($content))
1989
		{
1990
			$button = @key($content['button']);
1991
			unset($content['button']);
1992
			$content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p');
1993
			$content['owner'] = (string) (int) $content['owner'];
1994
			$content['cat_id'] = $this->config['cat_tab'] === 'Tree' ? $content['cat_id_tree'] : $content['cat_id'];
1995
			if ($this->config['private_cf_tab']) $content = (array)$content['private_cfs'] + $content;
1996
			unset($content['private_cfs']);
1997
1998
			switch($button)
1999
			{
2000
				case 'save':
2001
				case 'apply':
2002
					if ($content['presets_fields'])
2003
					{
2004
						// unset the duplicate_filed after submit because we don't need to warn user for second time about contact duplication
2005
						unset($content['presets_fields']);
2006
					}
2007
					// photo might be changed by ajax_upload_photo
2008
					if (!array_key_exists('jpegphoto', $content))
2009
					{
2010
						$content['photo_unchanged'] = true;	// hint no need to store photo
2011
					}
2012
					$links = false;
2013
					if (!$content['id'] && is_array($content['link_to']['to_id']))
2014
					{
2015
						$links = $content['link_to']['to_id'];
2016
					}
2017
					$fullname = $old_fullname = parent::fullname($content);
2018
					if ($content['id'] && $content['org_name'] && $content['change_org'])
2019
					{
2020
						$old_org_entry = $this->read($content['id']);
2021
						$old_fullname = ($old_org_entry['n_fn'] ? $old_org_entry['n_fn'] : parent::fullname($old_org_entry));
2022
					}
2023
					if ( $content['n_fn'] != $fullname ||  $fullname != $old_fullname)
2024
					{
2025
						unset($content['n_fn']);
2026
					}
2027
					// Country codes
2028
					foreach(array('adr_one', 'adr_two') as $c_prefix)
2029
					{
2030
						if ($content[$c_prefix.'_countrycode'] == '-custom-')
2031
						{
2032
							$content[$c_prefix.'_countrycode'] = null;
2033
						}
2034
					}
2035
					$content['msg'] = '';
2036
					$this->error = false;
2037
					foreach((array)$content['pre_save_callbacks'] as $callback)
2038
					{
2039
						try {
2040
							if (($success_msg = call_user_func_array($callback, array(&$content))))
2041
							{
2042
								$content['msg'] .= ($content['msg'] ? ', ' : '').$success_msg;
2043
							}
2044
						}
2045
						catch (Exception $ex) {
2046
							$content['msg'] .= ($content['msg'] ? ', ' : '').$ex->getMessage();
2047
							$button = 'apply';	// do not close dialog
2048
							$this->error = true;
2049
							break;
2050
						}
2051
					}
2052
					if ($this->error)
2053
					{
2054
						// error in pre_save_callbacks
2055
					}
2056
					elseif ($this->save($content))
2057
					{
2058
						$content['msg'] .= ($content['msg'] ? ', ' : '').lang('Contact saved');
2059
2060
						unset($content['jpegphoto'], $content['photo_unchanged']);
2061
2062
						foreach((array)$content['post_save_callbacks'] as $callback)
2063
						{
2064
							try {
2065
								if (($success_msg = call_user_func_array($callback, array(&$content))))
2066
								{
2067
									$content['msg'] .= ', '.$success_msg;
2068
								}
2069
							}
2070
							catch(Api\Exception\Redirect $r)
2071
							{
2072
								// catch it to continue execution and rethrow it later
2073
							}
2074
							catch (Exception $ex) {
2075
								$content['msg'] .= ', '.$ex->getMessage();
2076
								$button = 'apply';	// do not close dialog
2077
								$this->error = true;
2078
								break;
2079
							}
2080
						}
2081
2082
						if ($content['change_org'] && $old_org_entry && ($changed = $this->changed_fields($old_org_entry,$content,true)) &&
2083
							($members = $this->org_similar($old_org_entry['org_name'],$changed)))
2084
						{
2085
							//foreach($changed as $name => $old_value) echo "<p>$name: '$old_value' --> '{$content[$name]}'</p>\n";
2086
							list($changed_members,$changed_fields,$failed_members) = $this->change_org($old_org_entry['org_name'],$changed,$content,$members);
2087
							if ($changed_members)
2088
							{
2089
								$content['msg'] .= ', '.lang('%1 fields in %2 other organisation member(s) changed',$changed_fields,$changed_members);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $changed_fields. ( Ignorable by Annotation )

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

2089
								$content['msg'] .= ', './** @scrutinizer ignore-call */ lang('%1 fields in %2 other organisation member(s) changed',$changed_fields,$changed_members);

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...
2090
							}
2091
							if ($failed_members)
2092
							{
2093
								$content['msg'] .= ', '.lang('failed to change %1 organisation member(s) (insufficent rights) !!!',$failed_members);
2094
							}
2095
						}
2096
					}
2097
					elseif($this->error === true)
0 ignored issues
show
introduced by
The condition $this->error === true is always false.
Loading history...
2098
					{
2099
						$content['msg'] = lang('Error: the entry has been updated since you opened it for editing!').'<br />'.
2100
							lang('Copy your changes to the clipboard, %1reload the entry%2 and merge them.','<a href="'.
2101
								htmlspecialchars(Egw::link('/index.php',array(
2102
									'menuaction' => 'addressbook.addressbook_ui.edit',
2103
									'contact_id' => $content['id'],
2104
								))).'">','</a>');
2105
						break;	// dont refresh the list
2106
					}
2107
					else
2108
					{
2109
						$content['msg'] = lang('Error saving the contact !!!').
2110
							($this->error ? ' '.$this->error : '');
2111
						$button = 'apply';	// to not leave the dialog
2112
					}
2113
					// writing links for new entry, existing ones are handled by the widget itself
2114
					if ($links && $content['id'])
2115
					{
2116
						Link::link('addressbook',$content['id'],$links);
2117
					}
2118
					// Update client side global datastore
2119
					$response = Api\Json\Response::get();
2120
					$response->generic('data', array('uid' => 'addressbook::'.$content['id'], 'data' => $content));
2121
					Framework::refresh_opener($content['msg'], 'addressbook', $content['id'],  $content['id'] ? 'edit' : 'add',
2122
						null, null, null, $this->error ? 'error' : 'success');
2123
2124
					// re-throw redirect exception, if there's no error
2125
					if (!$this->error && isset($r))
2126
					{
2127
						throw $r;
2128
					}
2129
					if ($button == 'save')
2130
					{
2131
						Framework::window_close();
2132
					}
2133
					else
2134
					{
2135
						Framework::message($content['msg'], $this->error ? 'error' : 'success');
2136
						unset($content['msg']);
2137
					}
2138
					$content['link_to']['to_id'] = $content['id'];
2139
					break;
2140
2141
				case 'delete':
2142
					$success = $failed = $action_msg = null;
2143
					if($this->action('delete',array($content['id']),false,$success,$failed,$action_msg,'',$content['msg']))
2144
					{
2145
						if ($GLOBALS['egw']->currentapp == 'addressbook')
2146
						{
2147
							Framework::refresh_opener(lang('Contact deleted'), 'addressbook', $content['id'], 'delete' );
2148
							Framework::window_close();
2149
						}
2150
						else
2151
						{
2152
							Framework::refresh_opener(lang('Contact deleted'), 'addressbook', $content['id'], null, 'addressbook');
2153
							Framework::window_close();
2154
						}
2155
					}
2156
					else
2157
					{
2158
						$content['msg'] = lang('Error deleting the contact !!!');
2159
					}
2160
					break;
2161
			}
2162
			$view = !$this->check_perms(Acl::EDIT, $content);
2163
		}
2164
		else
2165
		{
2166
			$content = array();
2167
			$contact_id = $_GET['contact_id'] ? $_GET['contact_id'] : ((int)$_GET['account_id'] ? 'account:'.(int)$_GET['account_id'] : 0);
2168
			$view = (boolean)$_GET['view'];
2169
			// new contact --> set some defaults
2170
			if ($contact_id && is_array($content = $this->read($contact_id)))
2171
			{
2172
				$contact_id = $content['id'];	// it could have been: "account:$account_id"
2173
				if (!$this->check_perms(Acl::EDIT, $content))
2174
				{
2175
					$view = true;
2176
				}
2177
			}
2178
			else // not found
2179
			{
2180
				$state = Api\Cache::getSession('addressbook', 'index');
2181
				// check if we create the new contact in an existing org
2182
				if (($org = $_GET['org']))
2183
				{
2184
					// arguments containing a comma get quoted by etemplate/js/nextmatch_action.js
2185
					// leading to error in Api\Db::column_data_implode, if not unquoted
2186
					if ($org[0] == '"') $org = substr($org, 1, -1);
2187
					$content = $this->read_org($org);
2188
				}
2189
				elseif ($state['grouped_view'] && !isset($this->grouped_views[$state['grouped_view']]))
2190
				{
2191
					$content = $this->read_org($state['grouped_view']);
2192
				}
2193
				else
2194
				{
2195
					if ($GLOBALS['egw_info']['user']['preferences']['common']['country'])
2196
					{
2197
						$content['adr_one_countrycode'] =
2198
							$GLOBALS['egw_info']['user']['preferences']['common']['country'];
2199
						$content['adr_one_countryname'] =
2200
							$GLOBALS['egw']->country->get_full_name($GLOBALS['egw_info']['user']['preferences']['common']['country']);
2201
						$content['adr_two_countrycode'] =
2202
							$GLOBALS['egw_info']['user']['preferences']['common']['country'];
2203
						$content['adr_two_countryname'] =
2204
							$GLOBALS['egw']->country->get_full_name($GLOBALS['egw_info']['user']['preferences']['common']['country']);
2205
					}
2206
					if ($this->prefs['fileas_default']) $content['fileas_type'] = $this->prefs['fileas_default'];
2207
				}
2208
				if (isset($_GET['owner']) && $_GET['owner'] !== '')
2209
				{
2210
					$content['owner'] = $_GET['owner'];
2211
				}
2212
				else
2213
				{
2214
					$content['owner'] = (string)($state['filter'] == 0 ? '' : $state['filter']);
2215
				}
2216
				$content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p');
2217
				if ($content['owner'] === '' || !($this->grants[$content['owner'] = (string) (int) $content['owner']] & Acl::ADD))
2218
				{
2219
					$content['owner'] = $this->default_addressbook;
2220
					$content['private'] = (int)$this->default_private;
2221
2222
					if (!($this->grants[$content['owner'] = (string) (int) $content['owner']] & Acl::ADD))
2223
					{
2224
						$content['owner'] = (string) $this->user;
2225
						$content['private'] = 0;
2226
					}
2227
				}
2228
				$new_type = array_keys($this->content_types);
2229
				// fetch active type to preset the type, if param typeid is not passed
2230
				$active_tid = Api\Cache::getSession('addressbook','active_tid');
2231
				if ($active_tid && strtoupper($active_tid) === 'D') unset($active_tid);
2232
				$content['tid'] = $_GET['typeid'] ? $_GET['typeid'] : ($active_tid?$active_tid:$new_type[0]);
2233
				foreach($this->get_contact_columns() as $field)
2234
				{
2235
					if ($_GET['presets'][$field])
2236
					{
2237
						if ($field=='email'||$field=='email_home')
2238
						{
2239
							$singleAddress = imap_rfc822_parse_adrlist($_GET['presets'][$field],'');
2240
							//error_log(__METHOD__.__LINE__.' Address:'.$singleAddress[0]->mailbox."@".$singleAddress[0]->host.", ".$singleAddress[0]->personal);
2241
							if (!(!is_array($singleAddress) || count($singleAddress)<1))
2242
							{
2243
								$content[$field] = $singleAddress[0]->mailbox."@".$singleAddress[0]->host;
2244
								if (!empty($singleAddress[0]->personal))
2245
								{
2246
									if (strpos($singleAddress[0]->personal,',')===false)
2247
									{
2248
										list($P_n_given,$P_n_family,$P_org_name)=explode(' ',$singleAddress[0]->personal,3);
2249
										if (strlen(trim($P_n_given))>0) $content['n_given'] = trim($P_n_given);
2250
										if (strlen(trim($P_n_family))>0) $content['n_family'] = trim($P_n_family);
2251
										if (strlen(trim($P_org_name))>0) $content['org_name'] = trim($P_org_name);
2252
									}
2253
									else
2254
									{
2255
										list($P_n_family,$P_other)=explode(',',$singleAddress[0]->personal,2);
2256
										if (strlen(trim($P_n_family))>0) $content['n_family'] = trim($P_n_family);
2257
										if (strlen(trim($P_other))>0)
2258
										{
2259
											list($P_n_given,$P_org_name)=explode(',',$P_other,2);
2260
											if (strlen(trim($P_n_given))>0) $content['n_given'] = trim($P_n_given);
2261
											if (strlen(trim($P_org_name))>0) $content['org_name'] = trim($P_org_name);
2262
										}
2263
									}
2264
								}
2265
							}
2266
							else
2267
							{
2268
								$content[$field] = $_GET['presets'][$field];
2269
							}
2270
						}
2271
						else
2272
						{
2273
							$content[$field] = $_GET['presets'][$field];
2274
						}
2275
					}
2276
				}
2277
				if (isset($_GET['presets']))
2278
				{
2279
					foreach(array('email','email_home','n_family','n_given','org_name') as $field)
2280
					{
2281
						if (!empty($content[$field]))
2282
						{
2283
							//Set the presets fields in content in order to be able to use them later in client side for checking duplication only on first time load
2284
							// after save/apply we unset them
2285
							$content['presets_fields'][]= $field;
2286
							break;
2287
						}
2288
					}
2289
					if (empty($content['n_fn'])) $content['n_fn'] = $this->fullname($content);
2290
				}
2291
				$content['creator'] = $this->user;
2292
				$content['created'] = $this->now_su;
2293
				unset($state);
2294
				//_debug_array($content);
2295
			}
2296
2297
			if ($_GET['msg']) $content['msg'] = strip_tags($_GET['msg']);	// dont allow HTML!
2298
2299
			if($content && $_GET['makecp'])	// copy the contact
2300
			{
2301
				$this->copy_contact($content);
2302
				$content['msg'] = lang('%1 copied - the copy can now be edited', lang(Link::get_registry('addressbook','entry')));
2303
				$view = false;
2304
			}
2305
			else
2306
			{
2307
				if ($contact_id && is_numeric($contact_id)) $content['link_to']['to_id'] = $contact_id;
2308
			}
2309
			// automatic link new entries to entries specified in the url
2310
			if (!$contact_id && isset($_REQUEST['link_app']) && isset($_REQUEST['link_id']) && !is_array($content['link_to']['to_id']))
2311
			{
2312
				$link_ids = is_array($_REQUEST['link_id']) ? $_REQUEST['link_id'] : array($_REQUEST['link_id']);
2313
				foreach(is_array($_REQUEST['link_app']) ? $_REQUEST['link_app'] : array($_REQUEST['link_app']) as $n => $link_app)
2314
				{
2315
					$link_id = $link_ids[$n];
2316
					if (preg_match('/^[a-z_0-9-]+:[:a-z_0-9-]+$/i',$link_app.':'.$link_id))	// gard against XSS
2317
					{
2318
						Link::link('addressbook',$content['link_to']['to_id'],$link_app,$link_id);
2319
					}
2320
				}
2321
			}
2322
		}
2323
		if ($content['id'])
2324
		{
2325
			// last and next calendar date
2326
			$dates = current($this->read_calendar(array($content['account_id'] ? $content['account_id'] : 'c'.$content['id']),false));
2327
			if(is_array($dates)) $content += $dates;
2328
		}
2329
2330
		// Registry has view_id as contact_id, so set it (custom fields uses it)
2331
		$content['contact_id'] = $content['id'];
2332
2333
		// Avoid ID conflict with tree & selectboxes
2334
		$content['cat_id_tree'] = $content['cat_id'];
2335
2336
		// Avoid setting conflicts with private custom fields
2337
		$content['private_cfs'] = array();
2338
		foreach(Api\Storage\Customfields::get('addressbook', true) as $name => $cf)
2339
		{
2340
			if ($this->config['private_cf_tab'] && $cf['private'] && isset($content['#'.$name]))
2341
			{
2342
				$content['private_cfs']['#'.$name] = $content['#'.$name];
2343
			}
2344
		}
2345
2346
		// how to display addresses
2347
		$content['addr_format']  = $this->addr_format_by_country($content['adr_one_countryname']);
2348
		$content['addr_format2'] = $this->addr_format_by_country($content['adr_two_countryname']);
2349
2350
		//_debug_array($content);
2351
		$readonlys['button[delete]'] = !$content['owner'] || !$this->check_perms(Acl::DELETE,$content);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$readonlys was never initialized. Although not strictly required by PHP, it is generally a good practice to add $readonlys = array(); before regardless.
Loading history...
2352
		$readonlys['button[copy]'] = $readonlys['button[edit]'] = $readonlys['button[vcard]'] = true;
2353
		$readonlys['button[save]'] = $readonlys['button[apply]'] = $view;
2354
		if ($view)
2355
		{
2356
			$readonlys['__ALL__'] = true;
2357
			$readonlys['button[cancel]'] = false;
2358
		}
2359
2360
		$sel_options['fileas_type'] = $this->fileas_options($content);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
2361
		$sel_options['adr_one_countrycode']['-custom-'] = lang('Custom');
2362
		$sel_options['owner'] = $this->get_addressbooks(Acl::ADD);
2363
		if ($content['owner']) unset($sel_options['owner'][0]);	// do not offer to switch to accounts, as we do not support moving contacts to accounts
2364
		if ((string) $content['owner'] !== '')
2365
		{
2366
			if (!isset($sel_options['owner'][(int)$content['owner']]))
2367
			{
2368
				$sel_options['owner'][(int)$content['owner']] = !$content['owner'] ? lang('Accounts') :
2369
					Api\Accounts::username($content['owner']);
2370
			}
2371
			$readonlys['owner'] = !$content['owner'] || 		// dont allow to move accounts, as this mean deleting the user incl. all content he owns
2372
				$content['id'] && !$this->check_perms(Acl::DELETE,$content);	// you need delete rights to move an existing contact into an other addressbook
2373
		}
2374
		// set the unsupported fields from the backend to readonly
2375
		foreach($this->get_fields('unsupported',$content['id'],$content['owner']) as $field)
2376
		{
2377
			$readonlys[$field] = true;
2378
		}
2379
		// for editing own account, make all fields not allowed by own_account_acl readonly
2380
		if (!$this->is_admin() && !$content['owner'] && $content['account_id'] == $this->user && $this->own_account_acl && !$view)
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...
2381
		{
2382
			$readonlys['__ALL__'] = true;
2383
			$readonlys['button[cancel]'] = false;
2384
2385
			foreach($this->own_account_acl as $field)
2386
			{
2387
				$readonlys[$field] = false;
2388
			}
2389
			if (!$readonlys['jpegphoto'])
2390
			{
2391
				$readonlys = array_merge($readonlys, array(
2392
					'upload_photo' => false,
2393
					'delete_photo' => false,
2394
					'addressbook.edit.upload' => false
2395
				));
2396
			}
2397
			if (!$readonlys['pubkey'])
2398
			{
2399
				$readonlys['addressbook:'.$content['id'].':.files/pgp-pubkey.asc'] =
2400
				$readonlys['addressbook:'.$content['id'].':.files/smime-pubkey.crt'] = false;
2401
			}
2402
		}
2403
2404
		if (isset($readonlys['n_fileas'])) $readonlys['fileas_type'] = $readonlys['n_fileas'];
2405
		// disable not needed tabs
2406
		$readonlys['tabs']['cats'] = !($content['cat_tab'] = $this->config['cat_tab']);
2407
		$readonlys['tabs']['custom'] = !$this->customfields || $this->get_backend($content['id'],$content['owner']) == $this->so_accounts;
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
2408
		$readonlys['tabs']['custom_private'] = $readonlys['tabs']['custom'] || !$this->config['private_cf_tab'];
2409
		$readonlys['tabs']['distribution_list'] = !$content['distrib_lists'];#false;
2410
		$readonlys['tabs']['history'] = $this->contact_repository != 'sql' || !$content['id'] ||
2411
			$this->account_repository != 'sql' && $content['account_id'];
2412
		if (!$content['id']) $readonlys['button[delete]'] = !$content['id'];
2413
		if ($this->config['private_cf_tab']) $content['no_private_cfs'] = 0;
2414
		$readonlys['change_org'] = empty($content['org_name']) || $view;
2415
2416
		// for editing the own account (by a non-admin), enable only the fields allowed via the "own_account_acl"
2417
		if (!$content['owner'] && !$this->check_perms(Acl::EDIT, $content))
2418
		{
2419
			$this->_set_readonlys_for_own_account_acl($readonlys, $content['id']);
2420
		}
2421
		for($i = -23; $i<=23; $i++)
2422
		{
2423
			$tz[$i] = ($i > 0 ? '+' : '').$i;
2424
		}
2425
		$sel_options['tz'] = $tz;
2426
		$content['tz'] = $content['tz'] ? $content['tz'] : '0';
2427
		if (count($this->content_types) > 1)
2428
		{
2429
			foreach($this->content_types as $type => $data)
2430
			{
2431
				$sel_options['tid'][$type] = $data['name'];
2432
			}
2433
			$content['typegfx'] = Api\Html::image('addressbook',$this->content_types[$content['tid']]['options']['icon'],'',' width="16px" height="16px"');
2434
		}
2435
		else
2436
		{
2437
			$content['no_tid'] = true;
2438
		}
2439
2440
		$content['view'] = false;
2441
		$content['link_to'] = array(
2442
			'to_app' => 'addressbook',
2443
			'to_id'  => $content['link_to']['to_id'],
2444
		);
2445
2446
		// Links for deleted entries
2447
		if($content['tid'] == self::DELETED_TYPE)
2448
		{
2449
			$content['link_to']['show_deleted'] = true;
2450
			if(!$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge')
2451
			{
2452
				$readonlys['button[delete]'] = true;
2453
			}
2454
		}
2455
2456
		// Enable history
2457
		$this->setup_history($content, $sel_options);
2458
2459
		$content['photo'] = $this->photo_src($content['id'],$content['jpegphoto'],'',$content['etag']);
2460
2461
		if ($content['private']) $content['owner'] .= 'p';
2462
2463
		// for custom types, check if we have a custom edit template named "addressbook.edit.$type", $type is the name
2464
		if (in_array($content['tid'], array('n',self::DELETED_TYPE)) || !$this->tmpl->read('addressbook.edit.'.$this->content_types[$content['tid']]['name']))
2465
		{
2466
			$this->tmpl->read('addressbook.edit');
2467
		}
2468
2469
		// allow other apps to add tabs to addressbook edit
2470
		$preserve = $content;
2471
		$preserve['old_owner'] = $content['owner'];
2472
		unset($preserve['jpegphoto'], $content['jpegphoto']);	// unused and messes up json encoding (not utf-8)
2473
		$this->tmpl->setElementAttribute('tabs', 'add_tabs', true);
2474
		$tabs =& $this->tmpl->getElementAttribute('tabs', 'tabs');
2475
		if (($first_call = !isset($tabs)))
2476
		{
2477
			$tabs = array();
2478
		}
2479
		//error_log(__LINE__.': '.__METHOD__."() first_call=$first_call");
2480
		$hook_data = Api\Hooks::process(array('location' => 'addressbook_edit')+$content);
2481
		//error_log(__METHOD__."() hook_data=".array2string($hook_data));
2482
		foreach($hook_data as $extra_tabs)
2483
		{
2484
			if (!$extra_tabs) continue;
2485
2486
			foreach(isset($extra_tabs[0]) ? $extra_tabs : array($extra_tabs) as $extra_tab)
2487
			{
2488
				if ($extra_tab['data'] && is_array($extra_tab['data']))
2489
				{
2490
					$content = array_merge($content, $extra_tab['data']);
2491
				}
2492
				if ($extra_tab['preserve'] && is_array($extra_tab['preserve']))
2493
				{
2494
					$preserve = array_merge($preserve, $extra_tab['preserve']);
2495
				}
2496
				if ($extra_tab['readonlys'] && is_array($extra_tab['readonlys']))
2497
				{
2498
					$readonlys = array_merge($readonlys, $extra_tab['readonlys']);
2499
				}
2500
				// we must NOT add tabs and callbacks more then once!
2501
				if (!$first_call) continue;
2502
2503
				if (!empty($extra_tab['pre_save_callback']))
2504
				{
2505
					$preserve['pre_save_callbacks'][] = $extra_tab['pre_save_callback'];
2506
				}
2507
				if (!empty($extra_tab['post_save_callback']))
2508
				{
2509
					$preserve['post_save_callbacks'][] = $extra_tab['post_save_callback'];
2510
				}
2511
				if (!empty($extra_tab['label']) && !empty($extra_tab['name']))
2512
				{
2513
					$tabs[] = array(
2514
						'label' =>	$extra_tab['label'],
2515
						'template' =>	$extra_tab['name'],
2516
						'prepend' => $extra_tab['prepend'],
2517
					);
2518
				}
2519
				//error_log(__METHOD__."() changed tabs=".array2string($tabs));
2520
			}
2521
		}
2522
		return $this->tmpl->exec('addressbook.addressbook_ui.edit', $content, $sel_options, $readonlys, $preserve, 2);
2523
	}
2524
2525
	/**
2526
	 * Set the readonlys for non-admins editing their own account
2527
	 *
2528
	 * @param array &$readonlys
2529
	 * @param int $id
2530
	 */
2531
	function _set_readonlys_for_own_account_acl(&$readonlys,$id)
2532
	{
2533
		// regular fields depending on the backend
2534
		foreach($this->get_fields('supported',$id,0) as $field)
2535
		{
2536
			if (!$this->own_account_acl || !in_array($field,$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...
2537
			{
2538
				$readonlys[$field] = true;
2539
				switch($field)
2540
				{
2541
					case 'tel_work':
2542
					case 'tel_cell':
2543
					case 'tel_home':
2544
						$readonlys[$field.'2'] = true;
2545
						break;
2546
					case 'n_fileas':
2547
						$readonlys['fileas_type'] = true;
2548
						break;
2549
				}
2550
			}
2551
		}
2552
		// custom fields
2553
		if ($this->customfields)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
2554
		{
2555
			foreach(array_keys($this->customfields) as $name)
2556
			{
2557
				if (!$this->own_account_acl || !in_array('#'.$name,$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...
2558
				{
2559
					$readonlys['#'.$name] = true;
2560
				}
2561
			}
2562
		}
2563
		// links
2564
		if (!$this->own_account_acl || !in_array('link_to',$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...
2565
		{
2566
			$readonlys['link_to'] = true;
2567
		}
2568
	}
2569
2570
	/**
2571
	 * Doublicate check: returns similar contacts: same email or 2 of name, firstname, org
2572
	 *
2573
	 * Also update/return fileas options, if necessary.
2574
	 *
2575
	 * @param array $values contact values from form
2576
	 * @param string $name name of changed value, eg. "email"
2577
	 * @param int $own_id =0 own contact id, to not check against it
2578
	 * @return array with keys 'msg' => "EMail address exists, do you want to open contact?" (or null if not existing)
2579
	 * 	'data' => array of id => "full name (addressbook)" pairs
2580
	 *  'fileas_options'
2581
	 */
2582
	public function ajax_check_values($values, $name, $own_id=0)
2583
	{
2584
		$fields = explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']);
2585
		$threshold = (int)$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_threshold'];
2586
2587
		$ret = array('doublicates' => array(), 'msg' => null);
2588
2589
		// if email changed, check for doublicates
2590
		if (in_array($name, array('email', 'email_home')) && in_array('contact_'.$name, $fields))
2591
		{
2592
			if (preg_match(Etemplate\Widget\Url::EMAIL_PREG, $values[$name]))	// only search for real email addresses, to not return to many contacts
2593
			{
2594
				$contacts = parent::search(array(
2595
					'email' => $values[$name],
2596
					'email_home' => $values[$name],
2597
				), false, '', '', '', false, 'OR');
2598
			}
2599
		}
2600
		else
2601
		{
2602
			// only set fileas-options if other then email changed
2603
			$ret['fileas_options'] = array_values($this->fileas_options($values));
2604
			// Full options for et2
2605
			$ret['fileas_sel_options'] = $this->fileas_options($values);
2606
2607
			// if name, firstname or org changed and enough are specified, check for doublicates
2608
			$specified_count = 0;
2609
			foreach($fields as $field)
2610
			{
2611
				if($values[trim($field)])
2612
				{
2613
					$specified_count++;
2614
				}
2615
			}
2616
			if (in_array($name,$fields) && $specified_count >= $threshold)
2617
			{
2618
				$filter = array();
2619
				foreach($fields as $n)	// use email too, to exclude obvious false positives
2620
				{
2621
					if (!empty($values[$n])) $filter[$n] = $values[$n];
2622
				}
2623
				$contacts = parent::search('', false, '', '', '', false, 'AND', false, $filter);
2624
			}
2625
		}
2626
		if ($contacts)
2627
		{
2628
			foreach($contacts as $contact)
2629
			{
2630
				if ($own_id && $contact['id'] == $own_id) continue;
2631
2632
				$ret['doublicates'][$contact['id']] = $this->fileas($contact).' ('.
2633
					(!$contact['owner'] ? lang('Accounts') : ($contact['owner'] == $this->user ?
2634
					($contact['private'] ? lang('Private') : lang('Personal')) : Api\Accounts::username($contact['owner']))).')';
2635
			}
2636
			if ($ret['doublicates'])
2637
			{
2638
				$ret['msg'] = lang('Similar contacts found:').
2639
					"\n\n".implode("\n", $ret['doublicates'])."\n\n".
2640
					lang('Open for editing?');
2641
			}
2642
		}
2643
		//error_log(__METHOD__.'('.array2string($values).", '$name', $own_id) doublicates found ".array2string($ret['doublicates']));
2644
		Api\Json\Response::get()->data($ret);
2645
	}
2646
2647
	/**
2648
	 * CRM view
2649
	 *
2650
	 * @param array $content
2651
	 */
2652
	function view(array $content=null)
2653
	{
2654
		// CRM list comes from content, request, or preference
2655
		$crm_list = $content['crm_list'] ? $content['crm_list'] :
2656
			($_GET['crm_list'] ? $_GET['crm_list'] : $GLOBALS['egw_info']['user']['preferences']['addressbook']['crm_list']);
2657
		if(!$crm_list || $crm_list == '~edit~') $crm_list = 'infolog';
2658
2659
		if(is_array($content))
2660
		{
2661
			$button = key($content['button']);
2662
			switch ($content['toolbar'] ? $content['toolbar'] : $button)
2663
			{
2664
				case 'vcard':
2665
					Egw::redirect_link('/index.php','menuaction=addressbook.uivcard.out&ab_id=' .$content['id']);
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
2666
2667
				case 'cancel':
2668
					Egw::redirect_link('/index.php','menuaction=addressbook.addressbook_ui.index&ajax=true');
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
2669
2670
				case 'delete':
2671
					Egw::redirect_link('/index.php',array(
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
2672
						'menuaction' => 'addressbook.addressbook_ui.index',
2673
						'msg' => $this->delete($content) ? lang('Contact deleted') : lang('Error deleting the contact !!!'),
2674
					));
2675
2676
				case 'next':
2677
					$inc = 1;
2678
					// fall through
2679
				case 'back':
2680
					if (!isset($inc)) $inc = -1;
2681
					// get next/previous contact in selection
2682
					$query = Api\Cache::getSession('addressbook', 'index');
2683
					$query['start'] = $content['index'] + $inc;
2684
					$query['num_rows'] = 1;
2685
					$rows = $readonlys = array();
2686
					$num_rows = $this->get_rows($query, $rows, $readonlys, true);
2687
					//error_log(__METHOD__."() get_rows()=$num_rows rows=".array2string($rows));
2688
					$contact_id = $rows[0];
2689
					if(!$contact_id || !is_array($content = $this->read($contact_id)))
2690
					{
2691
						Egw::redirect_link('/index.php',array(
2692
							'menuaction' => 'addressbook.addressbook_ui.index',
2693
							'msg' => $content,
2694
							'ajax' => 'true'
2695
						));
2696
					}
2697
					$content['index'] = $query['start'];
2698
2699
					// List nextmatch is already there, just update the filter
2700
					if($contact_id && Api\Json\Request::isJSONRequest())
2701
					{
2702
						switch($crm_list)
2703
						{
2704
							case 'infolog-organisation':
2705
								$contact_id = $this->get_all_org_contacts($contact_id);
2706
								// Fall through
2707
							case 'infolog':
2708
							case 'tracker':
2709
							default:
2710
								Api\Json\Response::get()->apply('app.addressbook.view_set_list',Array(Array('action'=>'addressbook', 'action_id' => $contact_id)));
2711
								break;
2712
						}
2713
2714
						// Clear contact_id, it's used as a flag to send the list
2715
						unset($contact_id);
2716
					}
2717
					break;
2718
			}
2719
		}
2720
		else
2721
		{
2722
			// allow to search eg. for a phone number
2723
			if (isset($_GET['search']))
2724
			{
2725
				$query = Api\Cache::getSession('addressbook', 'index');
2726
				$query['search'] = $_GET['search'];
2727
				unset($_GET['search']);
2728
				// reset all filters
2729
				unset($query['advanced_search']);
2730
				$query['col_filter'] = array();
2731
				$query['filter'] = $query['filter2'] = $query['cat_id'] = '';
2732
				Api\Cache::setSession('addressbook', 'index', $query);
2733
				$query['start'] = 0;
2734
				$query['num_rows'] = 1;
2735
				$rows = $readonlys = array();
2736
				$num_rows = $this->get_rows($query, $rows, $readonlys, true);
2737
				$_GET['contact_id'] = array_shift($rows);
2738
				$_GET['index'] = 0;
2739
			}
2740
			$contact_id = $_GET['contact_id'] ? $_GET['contact_id'] : ((int)$_GET['account_id'] ? 'account:'.(int)$_GET['account_id'] : 0);
2741
			if(!$contact_id || !is_array($content = $this->read($contact_id)))
2742
			{
2743
				Egw::redirect_link('/index.php',array(
2744
					'menuaction' => 'addressbook.addressbook_ui.index',
2745
					'msg' => $content,
2746
					'ajax' => 'true'
2747
				)+(isset($_GET['search']) ? array('search' => $_GET['search']) : array()));
2748
			}
2749
			if (isset($_GET['index']))
2750
			{
2751
				$content['index'] = (int)$_GET['index'];
2752
				// get number of rows to determine if we can have a next button
2753
				$query = Api\Cache::getSession('addressbook', 'index');
2754
				$query['start'] = $content['index'];
2755
				$query['num_rows'] = 1;
2756
				$rows = $readonlys = array();
2757
				$num_rows = $this->get_rows($query, $rows, $readonlys, true);
2758
			}
2759
		}
2760
		$content['jpegphoto'] = !empty($content['jpegphoto']);	// unused and messes up json encoding (not utf-8)
2761
2762
		// make everything not explicit mentioned readonly
2763
		$readonlys['__ALL__'] = true;
2764
		$readonlys['photo'] = $readonlys['button[cancel]'] = $readonlys['button[copy]'] =
2765
			$readonlys['button[ok]'] = $readonlys['button[more]'] = $readonlys['toolbar'] = false;
2766
2767
		foreach(array_keys($this->contact_fields) as $key)
2768
		{
2769
			if (in_array($key,array('tel_home','tel_work','tel_cell','tel_fax')))
2770
			{
2771
				$content[$key.'2'] = $content[$key];
2772
			}
2773
		}
2774
2775
		// respect category permissions
2776
		if(!empty($content['cat_id']))
2777
		{
2778
			$content['cat_id'] = $this->categories->check_list(Acl::READ,$content['cat_id']);
2779
		}
2780
		$content['cat_id_tree'] = $content['cat_id'];
2781
2782
		$content['view'] = true;
2783
		$content['link_to'] = array(
2784
			'to_app' => 'addressbook',
2785
			'to_id'  => $content['id'],
2786
		);
2787
		// Links for deleted entries
2788
		if($content['tid'] == self::DELETED_TYPE)
2789
		{
2790
			$content['link_to']['show_deleted'] = true;
2791
		}
2792
		$readonlys['button[delete]'] = !$content['owner'] || !$this->check_perms(Acl::DELETE,$content);
2793
		$readonlys['button[edit]'] = !$this->check_perms(Acl::EDIT,$content);
2794
2795
		// how to display addresses
2796
		$content['addr_format']  = $this->addr_format_by_country($content['adr_one_countryname']);
2797
		$content['addr_format2'] = $this->addr_format_by_country($content['adr_two_countryname']);
2798
2799
		$sel_options['fileas_type'][$content['fileas_type']] = $this->fileas($content);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
2800
		$sel_options['owner'] = $this->get_addressbooks();
2801
		for($i = -23; $i<=23; $i++)
2802
		{
2803
			$tz[$i] = ($i > 0 ? '+' : '').$i;
2804
		}
2805
		$sel_options['tz'] = $tz;
2806
		$content['tz'] = $content['tz'] ? $content['tz'] : 0;
2807
		if (count($this->content_types) > 1)
2808
		{
2809
			foreach($this->content_types as $type => $data)
2810
			{
2811
				$sel_options['tid'][$type] = $data['name'];
2812
			}
2813
			$content['typegfx'] = Api\Html::image('addressbook',$this->content_types[$content['tid']]['options']['icon'],'',' width="16px" height="16px"');
2814
		}
2815
		else
2816
		{
2817
			$content['no_tid'] = true;
2818
		}
2819
		$this->tmpl->read('addressbook.view');
2820
		/*if (!$this->tmpl->read($this->content_types[$content['tid']]['options']['template'] ? $this->content_types[$content['tid']]['options']['template'] : 'addressbook.edit'))
2821
		{
2822
			$content['msg']  = lang('WARNING: Template "%1" not found, using default template instead.', $this->content_types[$content['tid']]['options']['template'])."\n";
2823
			$content['msg'] .= lang('Please update the templatename in your customfields section!');
2824
			$this->tmpl->read('addressbook.edit');
2825
		}*/
2826
		if ($this->private_addressbook && $content['private'] && $content['owner'] == $this->user)
2827
		{
2828
			$content['owner'] .= 'p';
2829
		}
2830
		$this->tmpl->set_cell_attribute('change_org','disabled',true);
0 ignored issues
show
Deprecated Code introduced by
The function EGroupware\Api\Etemplate::set_cell_attribute() has been deprecated: use setElementAttribute($name, $attr, $val) ( Ignorable by Annotation )

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

2830
		/** @scrutinizer ignore-deprecated */ $this->tmpl->set_cell_attribute('change_org','disabled',true);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2831
2832
		// Prevent double countries - invalid code blanks it, disabling doesn't work
2833
		$content['adr_one_countrycode'] = '-';
2834
		$content['adr_two_countrycode'] = '-';
2835
2836
		// Enable history
2837
		$this->setup_history($content, $sel_options);
2838
2839
		// disable not needed tabs
2840
		$readonlys['tabs']['cats'] = !($content['cat_tab'] = $this->config['cat_tab']);
2841
		$readonlys['tabs']['custom'] = !$this->customfields;
2842
		$readonlys['tabs']['custom_private'] = !$this->customfields || !$this->config['private_cf_tab'];
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
2843
		$readonlys['tabs']['distribution_list'] = !$content['distrib_lists'];#false;
2844
		$readonlys['tabs']['history'] = $this->contact_repository != 'sql' || !$content['id'] ||
2845
			$this->account_repository != 'sql' && $content['account_id'];
2846
		if ($this->config['private_cf_tab']) $content['no_private_cfs'] = 0;
2847
2848
		// last and next calendar date
2849
		if (!empty($content['id'])) $dates = current($this->read_calendar(array($content['account_id'] ? $content['account_id'] : 'c'.$content['id']),false));
2850
		if(is_array($dates)) $content += $dates;
2851
2852
		// Disable importexport
2853
		$GLOBALS['egw_info']['flags']['disable_importexport']['export'] = true;
2854
		$GLOBALS['egw_info']['flags']['disable_importexport']['merge'] = true;
2855
2856
		// set id for automatic linking via quick add
2857
		$GLOBALS['egw_info']['flags']['currentid'] = $content['id'];
2858
2859
		// load app.css for addressbook explicit, as addressbook_view hooks changes currentapp!
2860
		Framework::includeCSS('addressbook', 'app');
2861
2862
		// dont show an app-header
2863
		$GLOBALS['egw_info']['flags']['app_header'] = '';
2864
2865
		$actions = array(
2866
			'open' => array(
2867
				'caption' => 'Open',
2868
				'toolbarDefault' => true,
2869
			),
2870
			'copy' => 'Copy',
2871
			'delete' => array(
2872
				'caption' => 'Delete',
2873
				'confirm' => 'Delete this entry',
2874
			),
2875
			'cancel' => array(
2876
				'caption' => 'Cancel',
2877
				'toolbarDefault' => true,
2878
				'icon' => 'close'
2879
			),
2880
			'back' => array(
2881
				'caption' => 'Back',
2882
				'toolbarDefault' => true,
2883
			),
2884
			'next' => array(
2885
				'caption' => 'Next',
2886
				'toolbarDefault' => true,
2887
			),
2888
		);
2889
		if (!isset($content['index']) || !$content['index'])
2890
		{
2891
			unset($actions['back']);
2892
		}
2893
		if (!isset($content['index']) || $content['index'] >= $num_rows-1)
2894
		{
2895
			unset($actions['next']);
2896
		}
2897
		$this->tmpl->setElementAttribute('toolbar', 'actions', $actions);
2898
2899
		// always show sidebox, as it contains contact-data
2900
		unset($GLOBALS['egw_info']['user']['preferences']['common']['auto_hide_sidebox']);
2901
2902
		// need to load list's app.js now, as exec calls header before other app can include it
2903
		Framework::includeJS('/'.$crm_list.'/js/app.js');
2904
2905
		$this->tmpl->exec('addressbook.addressbook_ui.view',$content,$sel_options,$readonlys,array(
2906
			'id' => $content['id'],
2907
			'index' => $content['index'],
2908
			'crm_list' => $crm_list
2909
		));
2910
2911
		// Only load this on first time - we're using AJAX, so it stays there through submits.
2912
		// Sending it again (via ajax) will break the addressbook.view etemplate2
2913
		if($contact_id)
2914
		{
2915
			// Show for whole organisation, not just selected contact
2916
			if($crm_list == 'infolog-organisation')
2917
			{
2918
				$crm_list = str_replace('-organisation','',$crm_list);
2919
				$_query = Api\Cache::getSession('addressbook', 'index');
0 ignored issues
show
Unused Code introduced by
The assignment to $_query is dead and can be removed.
Loading history...
2920
				$content['id'] = $this->get_all_org_contacts($content['id']);
2921
			}
2922
			Api\Hooks::single(array(
2923
				'location' => 'addressbook_view',
2924
				'ab_id'    => $content['id']
2925
			),$crm_list);
2926
		}
2927
	}
2928
2929
	/**
2930
	 * Get all the contact IDs in the given contact's organisation
2931
	 *
2932
	 * @param int $contact_id
2933
	 * @param Array $query Optional base query
2934
	 *
2935
	 * @return array of contact IDs in the organisation
2936
	 */
2937
	function get_all_org_contacts($contact_id, $query = array())
2938
	{
2939
		$contact = $this->read($contact_id);
2940
2941
		// No org name, early return with just the contact
2942
		if(!$contact['org_name'])
2943
		{
2944
			return array($contact_id);
2945
		}
2946
2947
		$query['num_rows'] = -1;
2948
		$query['start'] = 0;
2949
		if(!array_key_exists('filter', $query))
2950
		{
2951
			$query['filter'] = '';
2952
		}
2953
		if(!is_array($query['col_filter']))
2954
		{
2955
			$query['col_filter'] = array();
2956
		}
2957
		$query['grouped_view'] = 'org_name:'.$contact['org_name'];
2958
2959
		$org_contacts = array();
2960
		$readonlys = null;
2961
		$this->get_rows($query,$org_contacts,$readonlys,true);	// true = only return the id's
2962
2963
		return $org_contacts ? $org_contacts : array($contact_id);
2964
	}
2965
2966
	/**
2967
	 * convert email-address in compose link
2968
	 *
2969
	 * @param string $email email-addresse
2970
	 * @return array/string array with get-params or mailto:$email, or '' or no mail addresse
0 ignored issues
show
Documentation Bug introduced by
The doc comment array/string at position 0 could not be parsed: Unknown type name 'array/string' at position 0 in array/string.
Loading history...
2971
	 */
2972
	function email2link($email)
2973
	{
2974
		if (strpos($email,'@') == false) return '';
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing strpos($email, '@') of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
2975
2976
		if($GLOBALS['egw_info']['user']['apps']['mail'])
2977
		{
2978
			return array(
2979
				'menuaction' => 'mail.mail_compose.compose',
2980
				'send_to'    => base64_encode($email)
2981
			);
2982
		}
2983
		if($GLOBALS['egw_info']['user']['apps']['felamimail'])
2984
		{
2985
			return array(
2986
				'menuaction' => 'felamimail.uicompose.compose',
2987
				'send_to'    => base64_encode($email)
2988
			);
2989
		}
2990
		if($GLOBALS['egw_info']['user']['apps']['email'])
2991
		{
2992
			return array(
2993
				'menuaction' => 'email.uicompose.compose',
2994
				'to' => $email,
2995
			);
2996
		}
2997
		return 'mailto:' . $email;
2998
	}
2999
3000
	/**
3001
	 * Extended search
3002
	 *
3003
	 * @param array $_content
3004
	 * @return string
3005
	 */
3006
	function search($_content=array())
3007
	{
3008
		if(!empty($_content))
3009
		{
3010
3011
			$_content['cat_id'] = $this->config['cat_tab'] === 'Tree' ? $_content['cat_id_tree'] : $_content['cat_id'];
3012
3013
			$response = Api\Json\Response::get();
3014
3015
			$query = Api\Cache::getSession('addressbook', 'index');
3016
3017
			if ($_content['button']['cancelsearch'])
3018
			{
3019
				unset($query['advanced_search']);
3020
			}
3021
			else
3022
			{
3023
				$query['advanced_search'] = array_intersect_key($_content,array_flip(array_merge($this->get_contact_columns(),array('operator','meth_select'))));
3024
				foreach ($query['advanced_search'] as $key => $value)
3025
				{
3026
					if(!$value) unset($query['advanced_search'][$key]);
3027
				}
3028
				// Skip n_fn, it causes problems in sql
3029
				unset($query['advanced_search']['n_fn']);
3030
			}
3031
			$query['search'] = '';
3032
			// store the index state in the session
3033
			Api\Cache::setSession('addressbook', 'index', $query);
3034
3035
			// store the advanced search in the session to call it again
3036
			Api\Cache::setSession('addressbook', 'advanced_search', $query['advanced_search']);
3037
3038
			// Update client / nextmatch with filters, or clear
3039
			$response->call("app.addressbook.adv_search", array('advanced_search' => $_content['button']['search'] ? $query['advanced_search'] : ''));
3040
			if ($_content['button']['cancelsearch'])
3041
			{
3042
				Framework::window_close ();
3043
3044
				// No need to reload popup
3045
				return;
3046
			}
3047
		}
3048
3049
		$GLOBALS['egw_info']['etemplate']['advanced_search'] = true;
3050
3051
		// initialize etemplate arrays
3052
		$sel_options = $readonlys = array();
3053
		$this->tmpl->read('addressbook.edit');
3054
		$content = Api\Cache::getSession('addressbook', 'advanced_search');
3055
		$content['n_fn'] = $this->fullname($content);
3056
		// Avoid ID conflict with tree & selectboxes
3057
		$content['cat_id_tree'] = $content['cat_id'];
3058
3059
		for($i = -23; $i<=23; $i++)
3060
		{
3061
			$tz[$i] = ($i > 0 ? '+' : '').$i;
3062
		}
3063
		$sel_options['tz'] = $tz + array('' => lang('doesn\'t matter'));
3064
		$sel_options['tid'][] = lang('all');
3065
		//foreach($this->content_types as $type => $data) $sel_options['tid'][$type] = $data['name'];
3066
3067
		// configure search options
3068
		$sel_options['owner'] = $this->get_addressbooks(Acl::READ,lang('all'));
3069
		$sel_options['operator'] =  array(
3070
			'AND' => 'AND',
3071
			'OR' => 'OR',
3072
		);
3073
		$sel_options['meth_select'] = array(
3074
			'%'		=> lang('contains'),
3075
			false	=> lang('exact'),
3076
		);
3077
		if ($this->customfields)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
3078
		{
3079
			foreach($this->customfields as $name => $data)
3080
			{
3081
				if (substr($data['type'], 0, 6) == 'select' && !($data['rows'] > 1))
3082
				{
3083
					if (!isset($content['#'.$name])) $content['#'.$name] = '';
3084
					if(!isset($data['values'][''])) $sel_options['#'.$name][''] = lang('Select one');
3085
				}
3086
				// Make them not required, otherwise you can't search
3087
				$this->tmpl->setElementAttribute('#'.$name, 'needed', FALSE);
3088
			}
3089
		}
3090
		// configure edit template as search dialog
3091
		$readonlys['change_photo'] = true;
3092
		$readonlys['fileas_type'] = true;
3093
		$readonlys['creator'] = true;
3094
		// this setting will enable (and show) the search and cancel buttons, setting this to true will hide the before mentioned buttons completely
3095
		$readonlys['button'] = false;
3096
		// disable not needed tabs
3097
		$readonlys['tabs']['cats'] = !($content['cat_tab'] = $this->config['cat_tab']);
3098
		$readonlys['tabs']['custom'] = !$this->customfields;
3099
		$readonlys['tabs']['custom_private'] = !$this->customfields || !$this->config['private_cf_tab'];
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
3100
		$readonlys['tabs']['links'] = true;
3101
		$readonlys['tabs']['distribution_list'] = true;
3102
		$readonlys['tabs']['history'] = true;
3103
		// setting hidebuttons for content will hide the 'normal' addressbook edit dialog buttons
3104
		$content['hidebuttons'] = true;
3105
		$content['no_tid'] = true;
3106
		$content['showsearchbuttons'] = true; // enable search operation and search buttons| they're disabled by default
3107
3108
		if ($this->config['private_cf_tab']) $content['no_private_cfs'] = 0;
3109
3110
		$this->tmpl->set_cell_attribute('change_org','disabled',true);
0 ignored issues
show
Deprecated Code introduced by
The function EGroupware\Api\Etemplate::set_cell_attribute() has been deprecated: use setElementAttribute($name, $attr, $val) ( Ignorable by Annotation )

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

3110
		/** @scrutinizer ignore-deprecated */ $this->tmpl->set_cell_attribute('change_org','disabled',true);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
3111
		return $this->tmpl->exec('addressbook.addressbook_ui.search',$content,$sel_options,$readonlys,array(),2);
3112
	}
3113
3114
	/**
3115
	 * Check if there's a photo for given contact id. This is used for avatar widget
3116
	 * to set or unset delete button. If there's no uploaded photo it responses true.
3117
	 *
3118
	 * @param type $contact_id
3119
	 */
3120
	function ajax_noPhotoExists ($contact_id)
3121
	{
3122
		$response = Api\Json\Response::get();
3123
		$response->data((!($contact = $this->read($contact_id)) ||
3124
			empty($contact['photo']) &&	!(($contact['files'] & Api\Contacts::FILES_BIT_PHOTO) &&
3125
				($size = filesize($url=Api\Link::vfs_path('addressbook', $contact_id, Api\Contacts::FILES_PHOTO))))));
0 ignored issues
show
Unused Code introduced by
The assignment to $size is dead and can be removed.
Loading history...
3126
	}
3127
3128
	/**
3129
	 * Ajax method to update edited avatar photo via avatar widget
3130
	 *
3131
	 * @param string $etemplate_exec_id to update id, files, etag, ...
3132
	 * @param file string $file null means to delete
3133
	 */
3134
	function ajax_update_photo ($etemplate_exec_id, $file)
3135
	{
3136
		$et_request = Api\Etemplate\Request::read($etemplate_exec_id);
3137
		$response = Api\Json\Response::get();
3138
		if ($file)
3139
		{
3140
			$filteredFile = substr($file, strpos($file, ",")+1);
3141
			// resize photo if wider then default width of 240pixel (keeping aspect ratio)
3142
			$decoded = $this->resize_photo(base64_decode($filteredFile));
3143
		}
3144
		$response->data(true);
3145
		// add photo into current eT2 request
3146
		$et_request->preserv = array_merge($et_request->preserv, array(
3147
			'jpegphoto' => is_null($file) ? $file : $decoded,
3148
			'photo_unchanged' => false,	// hint photo is changed
3149
		));
3150
	}
3151
3152
	/**
3153
	 * Callback for vfs-upload widgets for PGP and S/Mime pubkey
3154
	 *
3155
	 * @param array $file
3156
	 * @param string $widget_id
3157
	 * @param Api\Etemplate\Request $request eT2 request eg. to access attribute $content
3158
	 * @param Api\Json\Response $response
3159
	 */
3160
	public function pubkey_uploaded(array $file, $widget_id, Api\Etemplate\Request $request, Api\Json\Response $response)
3161
	{
3162
		//error_log(__METHOD__."(".array2string($file).", ...) widget_id=$widget_id, id=".$request->content['id'].", files=".$request->content['files']);
3163
		unset($file, $response);	// not used, but required by function signature
3164
		list(,,$path) = explode(':', $widget_id);
3165
		$bit = $path === Api\Contacts::FILES_PGP_PUBKEY ? Api\Contacts::FILES_BIT_PGP_PUBKEY : Api\Contacts::FILES_BIT_SMIME_PUBKEY;
3166
		if (!($request->content['files'] & $bit) && $this->check_perms(Acl::EDIT, $request->content))
3167
		{
3168
			$content = $request->content;
3169
			$content['files'] |= $bit;
3170
			$content['photo_unchanged'] = true;	// hint no need to store photo
3171
			if ($this->save($content))
3172
			{
3173
				$changed = array_diff_assoc($content, $request->content);
3174
				//error_log(__METHOD__."() changed=".array2string($changed));
3175
				$request->content = $content;
3176
				// need to update preserv, as edit stores content there too and we would get eg. an contact modified error when trying to store
3177
				$request->preserv = array_merge($request->preserv, $changed);
3178
			}
3179
		}
3180
	}
3181
3182
	/**
3183
	 * Migrate contacts to or from LDAP (called by Admin >> Addressbook >> Site configuration (Admin only)
3184
	 *
3185
	 */
3186
	function migrate2ldap()
3187
	{
3188
		$GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Migration to LDAP');
3189
		echo $GLOBALS['egw']->framework->header();
3190
		echo $GLOBALS['egw']->framework->navbar();
3191
3192
		if (!$this->is_admin())
3193
		{
3194
			echo '<h1>'.lang('Permission denied !!!')."</h1>\n";
3195
		}
3196
		else
3197
		{
3198
			parent::migrate2ldap($_GET['type']);
3199
			echo '<p style="margin-top: 20px;"><b>'.lang('Migration finished')."</b></p>\n";
3200
		}
3201
		echo $GLOBALS['egw']->framework->footer();
3202
	}
3203
3204
	/**
3205
	 * Set n_fileas (and n_fn) in contacts of all users  (called by Admin >> Addressbook >> Site configuration (Admin only)
3206
	 *
3207
	 * If $_GET[all] all fileas fields will be set, if !$_GET[all] only empty ones
3208
	 *
3209
	 */
3210
	function admin_set_fileas()
3211
	{
3212
		Api\Translation::add_app('admin');
3213
		$GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Contact maintenance');
3214
		echo $GLOBALS['egw']->framework->header();
3215
		echo $GLOBALS['egw']->framework->navbar();
3216
3217
		// check if user has admin rights AND if a valid fileas type is given (Security)
3218
		if (!$this->is_admin() || $_GET['type'] != '' && !in_array($_GET['type'],$this->fileas_types))
3219
		{
3220
			echo '<h1>'.lang('Permission denied !!!')."</h1>\n";
3221
		}
3222
		else
3223
		{
3224
			$errors = null;
3225
			$updated = parent::set_all_fileas($_GET['type'],(boolean)$_GET['all'],$errors,true);	// true = ignore Acl
3226
			echo '<p style="margin-top: 20px;"><b>'.lang('%1 contacts updated (%2 errors).',$updated,$errors)."</b></p>\n";
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $updated. ( Ignorable by Annotation )

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

3226
			echo '<p style="margin-top: 20px;"><b>'./** @scrutinizer ignore-call */ lang('%1 contacts updated (%2 errors).',$updated,$errors)."</b></p>\n";

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...
3227
		}
3228
		echo $GLOBALS['egw']->framework->footer();
3229
	}
3230
3231
	/**
3232
	 * Cleanup all contacts of all users (called by Admin >> Addressbook >> Site configuration (Admin only)
3233
	 *
3234
	 */
3235
	function admin_set_all_cleanup()
3236
	{
3237
		Api\Translation::add_app('admin');
3238
		$GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Contact maintenance');
3239
		echo $GLOBALS['egw']->framework->header();
3240
		echo $GLOBALS['egw']->framework->navbar();
3241
3242
		// check if user has admin rights (Security)
3243
		if (!$this->is_admin())
3244
		{
3245
			echo '<h1>'.lang('Permission denied !!!')."</h1>\n";
3246
		}
3247
		else
3248
		{
3249
			$errors = null;
3250
			$updated = parent::set_all_cleanup($errors,true);	// true = ignore Acl
3251
			echo '<p style="margin-top: 20px;"><b>'.lang('%1 contacts updated (%2 errors).',$updated,$errors)."</b></p>\n";
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $updated. ( Ignorable by Annotation )

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

3251
			echo '<p style="margin-top: 20px;"><b>'./** @scrutinizer ignore-call */ lang('%1 contacts updated (%2 errors).',$updated,$errors)."</b></p>\n";

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...
3252
		}
3253
		echo $GLOBALS['egw']->framework->footer();
3254
	}
3255
3256
	/**
3257
	* Set up history log widget
3258
	*/
3259
	protected function setup_history(&$content, &$sel_options)
3260
	{
3261
		if ($this->contact_repository == 'ldap' || !$content['id'] ||
3262
			$this->account_repository == 'ldap' && $content['account_id'])
3263
		{
3264
			return;	// no history for ldap as history table only allows integer id's
3265
		}
3266
		$content['history'] = array(
3267
			'id'	=>	$content['id'],
3268
			'app'	=>	'addressbook',
3269
			'status-widgets'	=>	array(
3270
				'owner'		=>	'select-account',
3271
				'creator'	=>	'select-account',
3272
				'created'	=>	'date-time',
3273
				'cat_id'	=>	'select-cat',
3274
				'adr_one_countrycode' => 'select-country',
3275
				'adr_two_countrycode' => 'select-country',
3276
			),
3277
		);
3278
3279
		foreach($this->content_types as $id => $settings)
3280
		{
3281
			$content['history']['status-widgets']['tid'][$id] = $settings['name'];
3282
		}
3283
		$sel_options['status'] = $this->contact_fields;
3284
3285
		// custom fields no longer need to be added, historylog-widget "knows" about them
3286
	}
3287
}
3288