Test Setup Failed
Push — master ( 65294a...859093 )
by Nathan
21:42
created

admin_account::copy()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 4
nop 3
dl 0
loc 26
rs 9.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware: Admin app UI: edit/add account
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <[email protected]>
7
 * @package admin
8
 * @copyright (c) 2014-19 by Ralf Becker <[email protected]>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 */
11
12
use EGroupware\Api;
13
use EGroupware\Api\Acl;
14
use EGroupware\Api\Etemplate;
15
use EGroupware\Api\Framework;
16
17
/**
18
 * UI for admin: edit/add account
19
 */
20
class admin_account
21
{
22
	/**
23
	 * Functions callable via menuaction
24
	 *
25
	 * @var array
26
	 */
27
	public $public_functions = array(
28
		'delete' => true,
29
	);
30
31
	// Copying account uses addressbook fields, but we explicitly clear these
32
	protected static $copy_clear_fields = array(
33
		'account_firstname','account_lastname','account_fullname', 'person_id',
34
		'account_id','account_lid',
35
		'account_lastlogin','accountlastloginfrom','account_lastpwd_change'
36
	);
37
38
	/**
39
	 * Hook to edit account data via "Account" tab in addressbook edit dialog
40
	 *
41
	 * @param array $content
42
	 * @return array
43
	 * @throws Api\Exception\NotFound
44
	 */
45
	public function addressbook_edit(array $content)
46
	{
47
		if ((string)$content['owner'] === '0' && $GLOBALS['egw_info']['user']['apps']['admin'])
48
		{
49
			$deny_edit = $content['account_id'] ? $GLOBALS['egw']->acl->check('account_access', 16, 'admin') :
50
				$GLOBALS['egw']->acl->check('account_access', 4, 'admin');
51
			//error_log(__METHOD__."() contact_id=$content[contact_id], account_id=$content[account_id], deny_edit=".array2string($deny_edit));
52
53
			if (!$content['account_id'] && $deny_edit) return;	// no right to add new accounts, should not happen by AB ACL
54
55
			// load our translations
56
			Api\Translation::add_app('admin');
57
58
			if ($content['id'])	// existing account
59
			{
60
				// invalidate account, before reading it, to code with changed to DB or LDAP outside EGw
61
				Api\Accounts::cache_invalidate((int)$content['account_id']);
62
				if (!($account = $GLOBALS['egw']->accounts->read($content['account_id'])))
63
				{
64
					throw new Api\Exception\NotFound('Account data NOT found!');
65
				}
66
				if ($account['account_expires'] == -1) $account['account_expires'] = '';
67
				unset($account['account_pwd']);	// do NOT send to client
68
				$account['account_groups'] = array_keys($account['memberships']);
69
				$acl = new Acl($content['account_id']);
70
				$acl->read_repository();
71
				$account['anonymous'] = $acl->check('anonymous', 1, 'phpgwapi');
72
				$account['changepassword'] = !$acl->check('nopasswordchange', 1, 'preferences');
73
				$auth = new Api\Auth();
74
				if (($account['account_lastpwd_change'] = $auth->getLastPwdChange($account['account_lid'])) === false)
75
				{
76
					$account['account_lastpwd_change'] = null;
77
				}
78
				$account['mustchangepassword'] = isset($account['account_lastpwd_change']) &&
79
					(string)$account['account_lastpwd_change'] === '0';
80
			}
81
			else	// new account
82
			{
83
				$account = array(
84
					'account_status' => 'A',
85
					'account_groups' => array(),
86
					'anonymous' => false,
87
					'changepassword' => true,	//old default: (bool)$GLOBALS['egw_info']['server']['change_pwd_every_x_days'],
88
					'mustchangepassword' => false,
89
					'account_primary_group' => $GLOBALS['egw']->accounts->name2id('Default'),
90
					'homedirectory' => $GLOBALS['egw_info']['server']['ldap_account_home'],
91
					'loginshell' => $GLOBALS['egw_info']['server']['ldap_account_shell'],
92
				);
93
			}
94
			// should we show extra ldap attributes home-directory and login-shell
95
			$account['ldap_extra_attributes'] = $GLOBALS['egw_info']['server']['ldap_extra_attributes'] &&
96
				get_class($GLOBALS['egw']->accounts->backend) === 'EGroupware\\Api\\Accounts\\Ldap';
97
98
			$readonlys = array();
99
100
			// at least ADS does not allow to unset it and SQL backend does not implement it either
101
			if ($account['mustchangepassword'])
102
			{
103
				$readonlys['mustchangepassword'] = true;
104
			}
105
106
			if ($deny_edit)
107
			{
108
				foreach(array_keys($account) as $key)
109
				{
110
					$readonlys[$key] = true;
111
				}
112
				$readonlys['account_passwd'] = $readonlys['account_passwd2'] = true;
113
			}
114
			// save old values to only trigger save, if one of the following values change (contact data get saved anyway)
115
			$preserve = empty($content['id']) ? array() :
116
				array('old_account' => array_intersect_key($account, array_flip(array(
117
						'account_lid', 'account_status', 'account_groups', 'anonymous', 'changepassword',
118
						'mustchangepassword', 'account_primary_group', 'homedirectory', 'loginshell',
119
						'account_expires', 'account_firstname', 'account_lastname', 'account_email'))),
120
						'deny_edit' => $deny_edit);
121
122
			if($content && $_GET['copy'])
0 ignored issues
show
Bug Best Practice introduced by
The expression $content 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...
123
			{
124
				$this->copy($content, $account, $preserve);
125
			}
126
			return array(
127
				'name' => 'admin.account',
128
				'prepend' => true,
129
				'label' => 'Account',
130
				'data' => $account,
131
				'preserve' => $preserve,
132
				'readonlys' => $readonlys,
133
				'pre_save_callback' => $deny_edit ? null : 'admin_account::addressbook_pre_save',
134
			);
135
		}
136
	}
137
138
	/**
139
	 * Hook called by addressbook prior to saving addressbook data
140
	 *
141
	 * @param array &$content
142
	 * @throws Exception for errors
143
	 * @return string Success message
144
	 */
145
	public static function addressbook_pre_save(&$content)
146
	{
147
		if (!isset($content['mustchangepassword']))
148
		{
149
			$content['mustchangepassword'] = true;	// was readonly because already set
150
		}
151
		$content['account_firstname'] = $content['n_given'];
152
		$content['account_lastname'] = $content['n_family'];
153
		$content['account_email'] = $content['email'];
154
		if($content['account_passwd'] && $content['account_passwd'] !== $content['account_passwd_2'])
155
		{
156
			throw new Api\Exception\WrongUserinput('Passwords are not the same');
157
		}
158
		if (!empty($content['old_account']))
159
		{
160
			$old = array_diff_assoc($content['old_account'], $content);
161
			// array_diff_assoc compares everything as string (cast to string)
162
			if ($content['old_account']['account_groups'] != $content['account_groups'])
163
			{
164
				$old['account_groups'] = $content['old_account']['account_groups'];
165
			}
166
			if($content['account_passwd'])
167
			{
168
				// Don't put password into history
169
				$old['account_passwd'] = '';
170
			}
171
		}
172
		if ($content['deny_edit'] || $old === array())
173
		{
174
			return '';	// no need to save account data, if nothing changed
175
		}
176
		//error_log(__METHOD__."(".array2string($content).")");
177
		$account = array();
178
		foreach(array(
179
			// need to copy/rename some fields named different in account and contact
180
			'n_given' => 'account_firstname',
181
			'n_family' => 'account_lastname',
182
			'email' => 'account_email',
183
			'account_groups',
184
			// copy following fields to account
185
			'account_lid',
186
			'changepassword', 'anonymous', 'mustchangepassword',
187
			'account_passwd', 'account_passwd_2',
188
			'account_primary_group',
189
			'account_expires', 'account_status',
190
			'homedirectory', 'loginshell',
191
			'requested', 'requested_email', 'comment',	// admin_cmd documentation (EPL)
192
		) as $c_name => $a_name)
193
		{
194
			if (is_int($c_name)) $c_name = $a_name;
195
196
			// only record real changes
197
			if (isset($content['old_account']) &&
198
				// currently LDAP (and probably also AD and UCS) can not skip unchanged fields!
199
				get_class($GLOBALS['egw']->accounts->backend) === 'EGroupware\\Api\\Accounts\\Sql' &&
200
				(!isset($content[$c_name]) && $c_name !== 'account_expires' || // account_expires is not set when empty!
201
				$content['old_account'][$a_name] == $content[$c_name]))
202
			{
203
				continue;	// no change --> no need to log setting it to identical value
204
			}
205
206
			switch($a_name)
207
			{
208
				case 'account_expires':
209
				case 'account_status':
210
					$account['account_expires'] = $content['account_expires'] ? $content['account_expires'] :
211
						($content['account_status'] ? 'never' : 'already');
212
					break;
213
214
				case 'changepassword':	// boolean values: admin_cmd_edit_user understands '' as NOT set
215
				case 'anonymous':
216
				case 'mustchangepassword':
217
					$account[$a_name] = (boolean)$content[$c_name];
218
					break;
219
220
				default:
221
					$account[$a_name] = $content[$c_name];
222
					break;
223
			}
224
		}
225
		// Make sure primary group is in account groups
226
		if (isset($account['account_groups']) && $account['account_primary_group'] &&
227
			!in_array($account['account_primary_group'], (array)$account['account_groups']))
228
		{
229
			$account['account_groups'][] = $account['account_primary_group'];
230
		}
231
232
		$cmd = new admin_cmd_edit_user(array(
233
			'account' => (int)$content['account_id'],
234
			'set' => $account,
235
			'old' => $old,
236
		)+(array)$content['admin_cmd']);
237
		$cmd->run();
238
239
		Api\Json\Response::get()->call('egw.refresh', '', 'admin', $cmd->account, $content['account_id'] ? 'edit' : 'add');
240
241
		$addressbook_bo = new Api\Contacts();
242
		if (!($content['id'] = Api\Accounts::id2name($cmd->account, 'person_id')) ||
243
			!($contact = $addressbook_bo->read($content['id'])))
244
		{
245
			throw new Api\Exception\AssertionFailed("Can't find contact of just created account!");
246
		}
247
		// for a new account a new contact was created, need to merge that data with $content
248
		if (!$content['account_id'])
249
		{
250
			$content['account_id'] = $cmd->account;
251
			$content = array_merge($contact, $content);
252
		}
253
		else	// for updated account, we need to refresh etag
254
		{
255
			$content['etag'] = $contact['etag'];
256
		}
257
	}
258
259
	public function copy(array &$content, array &$account, array &$preserve)
260
	{
261
		// We skipped the addressbook copy, call it now
262
		$ab_ui = new addressbook_ui();
263
		$ab_ui->copy_contact($content, true);
264
265
		// copy_contact() reset the owner, fix it
266
		$content['owner'] = '0';
267
268
		// Explicitly, always clear these
269
		static $clear_content = Array(
270
			'n_family','n_given','n_middle','n_suffix','n_fn','n_fileas',
271
			'account_id','contact_id','id','etag','carddav_name','uid'
272
		);
273
		foreach($clear_content as $field)
274
		{
275
			$account[$field] ='';
276
			$preserve[$field] = '';
277
		}
278
		$account['link_to']['to_id'] = 0;
279
		unset($preserve['old_account']);
280
281
		// Never copy these on an account
282
		foreach(static::$copy_clear_fields as $field)
283
		{
284
			unset($account[$field]);
285
		}
286
	}
287
288
	/**
289
	 * Delete an account
290
	 *
291
	 * @param array $content =null
292
	 */
293
	public static function delete(array $content=null)
294
	{
295
		if (!is_array($content))
296
		{
297
			if (isset($_GET['contact_id']) && ($account_id = $GLOBALS['egw']->accounts->name2id((int)$_GET['contact_id'], 'person_id')))
298
			{
299
				$content = array(
300
					'account_id' => $account_id,
301
					'contact_id' => (int)$_GET['contact_id'],
302
				);
303
			}
304
			else
305
			{
306
				$content = array('account_id' => (int)$_GET['account_id']);
307
			}
308
			//error_log(__METHOD__."() \$_GET[account_id]=$_GET[account_id], \$_GET[contact_id]=$_GET[contact_id] content=".array2string($content));
309
		}
310
		if ($GLOBALS['egw']->acl->check('account_access',32,'admin') ||
311
			$GLOBALS['egw_info']['user']['account_id'] == $content['account_id'])
312
		{
313
			Framework::window_close(lang('Permission denied!!!'));
314
		}
315
		if ($content['delete'])
316
		{
317
			$cmd = new admin_cmd_delete_account(array(
318
				'account' => $content['account_id'],
319
				'new_user' => $content['new_owner'],
320
				'is_user' => $content['account_id'] > 0,
321
				'change_apps' => $content['delete_apps']
322
			) +  (array)$content['admin_cmd']);
323
			$msg = $cmd->run();
324
			if ($content['contact_id'])
325
			{
326
				Framework::refresh_opener($msg, 'addressbook', $content['contact_id'], 'delete');
327
			}
328
			else
329
			{
330
				Framework::refresh_opener($msg, 'admin', $content['account_id'], 'delete');
331
			}
332
			Framework::window_close();
333
		}
334
335
		$sel_options = array();
336
		$preserve = $content;
337
338
		// Get a count of entries owned by the user
339
		$counts = $GLOBALS['egw']->accounts->get_account_entry_counts($content['account_id']);
340
		foreach($counts as $app => $counts)
341
		{
342
			$entry = Api\Link::get_registry($app, 'entries');
343
			if(!$entry)
344
			{
345
				$entry = lang('Entries');
346
			}
347
			if($counts['total'] && Api\Hooks::exists('deleteaccount', $app))
348
			{
349
				$content['delete_apps'][] = $app;
350
				$sel_options['delete_apps'][] = array(
351
					'value' => $app,
352
					'label' => lang($app) . ': ' . $counts['total'] . ' '.$entry
0 ignored issues
show
Bug introduced by
Are you sure $entry of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

352
					'label' => lang($app) . ': ' . $counts['total'] . ' './** @scrutinizer ignore-type */ $entry
Loading history...
353
				);
354
			}
355
			else if ($counts['total'])
356
			{
357
				// These ones don't support the needed hook
358
				$content['counts'][] = array(
359
					'app' => $app,
360
					'count' => $counts['total'] . ' '.$entry
361
				);
362
			}
363
		}
364
		// Add filemanager home directory in as special case, hook is in the API
365
		if(Api\Vfs::file_exists('/home/'.$GLOBALS['egw']->accounts->id2name($content['account_id'])))
366
		{
367
			$app = 'filemanager';
368
			$sel_options['delete_apps'][] = array(
369
				'value' => $app,
370
				'label' => lang($app) . ': /home'
371
			);
372
			$content['delete_apps'][] = $app;
373
		}
374
375
		$tpl = new Etemplate('admin.account.delete');
376
		$tpl->exec('admin_account::delete', $content, $sel_options, array(), $preserve, 2);
377
	}
378
379
	/**
380
	 * Delete a group via ajax
381
	 *
382
	 * @param int $account_id
383
	 * @param String[] $data Optional data
384
	 * @param string $etemplate_exec_id to check against CSRF
385
	 */
386
	public static function ajax_delete_group($account_id, $data, $etemplate_exec_id)
387
	{
388
		Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
389
390
		$cmd = new admin_cmd_delete_account(Api\Accounts::id2name(Api\Accounts::id2name($account_id)), null, false, (array)$data['admin_cmd']);
391
		$msg = $cmd->run();
392
393
		Api\Json\Response::get()->call('egw.refresh', $msg, 'admin', $account_id, 'delete');
394
	}
395
396
	/**
397
	 * Check entered data and return error-msg via json data or null
398
	 *
399
	 * @param array $data values for account_id and account_lid
400
	 * @param string $changed name of addressbook widget triggering change eg. "email", "n_given" or "n_family"
401
	 */
402
	public static function ajax_check(array $data, $changed)
403
	{
404
		// warn if anonymous user is renamed, as it breaks eg. sharing and Collabora
405
		if ($changed == 'account_lid' && Api\Accounts::id2name($data['account_id']) === 'anonymous' && $data['account_lid'] !== 'anonymous')
406
		{
407
			Api\Json\Response::get()->data(lang("Renaming user 'anonymous' will break file sharing and Collabora Online Office!"));
408
			return;
409
		}
410
411
		// for 1. password field just check password complexity
412
		if ($changed == 'account_passwd')
413
		{
414
			$data['account_fullname'] = $data['account_firstname'].' '.$data['account_lastname'];
415
			if (($error = Api\Auth::crackcheck($data['account_passwd'], null, null, null, $data)))
416
			{
417
				$error .= "\n\n".lang('If you ignore that error as admin, you should check "%1"!', lang('Must change password upon next login'));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with lang('Must change password upon next login'). ( Ignorable by Annotation )

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

417
				$error .= "\n\n"./** @scrutinizer ignore-call */ lang('If you ignore that error as admin, you should check "%1"!', lang('Must change password upon next login'));

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...
418
			}
419
			Api\Json\Response::get()->data($error);
420
			return;
421
		}
422
		// generate default email address, but only for new Api\Accounts
423
		if (!$data['account_id'] && in_array($changed, array('n_given', 'n_family', 'account_lid')))
424
		{
425
			$email = Api\Accounts::email($data['account_firstname'], $data['account_lastname'], $data['account_lid']);
426
			if ($email && $email[0] != '@' && strpos($email, '@'))	// only add valid email addresses
427
			{
428
				Api\Json\Response::get()->assign('addressbook-edit_email', 'value', $email);
429
			}
430
		}
431
432
		if (!$data['account_lid'] && !$data['account_id']) return;	// makes no sense to check before
433
434
		// set home-directory when account_lid is entered, but only for new Api\Accounts
435
		if ($changed == 'account_lid' && !$data['account_id'] &&
436
			$GLOBALS['egw_info']['server']['ldap_extra_attributes'] &&
437
			$GLOBALS['egw_info']['server']['ldap_account_home'])
438
		{
439
			Api\Json\Response::get()->assign('addressbook-edit_homedirectory', 'value',
440
				$GLOBALS['egw_info']['server']['ldap_account_home'].'/'.preg_replace('/[^a-z0-9_.-]/i', '',
441
					Api\Translation::to_ascii($data['account_lid'])));
442
		}
443
444
		// set dummy membership to get no error about no members yet
445
		$data['account_memberships'] = array($data['account_primary_user'] = $GLOBALS['egw_info']['user']['account_primary_group']);
446
447
		try {
448
			$cmd = new admin_cmd_edit_user($data['account_id'], $data);
449
			$cmd->run(null, false, false, true);
450
		}
451
		catch(Exception $e)
452
		{
453
			Api\Json\Response::get()->data($e->getMessage());
454
		}
455
	}
456
}
457