1<?php
2/**
3 * EGroupware: Admin app UI: edit/add account
4 *
5 * @link http://www.egroupware.org
6 * @author Ralf Becker <rb@stylite.de>
7 * @package admin
8 * @copyright (c) 2014-19 by Ralf Becker <rb@stylite.de>
9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10 */
11
12use EGroupware\Api;
13use EGroupware\Api\Acl;
14use EGroupware\Api\Etemplate;
15use EGroupware\Api\Framework;
16
17/**
18 * UI for admin: edit/add account
19 */
20class 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'])
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['creator'] = $ab_ui->user;
279		$account['created'] = $ab_ui->now_su;
280		$account['modified'] = '';
281		$account['modifier'] = '';
282		$account['link_to']['to_id'] = 0;
283		unset($preserve['old_account']);
284
285		// Never copy these on an account
286		foreach(static::$copy_clear_fields as $field)
287		{
288			unset($account[$field]);
289		}
290	}
291
292	/**
293	 * Delete an account
294	 *
295	 * @param array $content =null
296	 */
297	public static function delete(array $content=null)
298	{
299		Api\Translation::add_app('admin');
300		if (!is_array($content))
301		{
302			if (isset($_GET['contact_id']) && ($account_id = $GLOBALS['egw']->accounts->name2id((int)$_GET['contact_id'], 'person_id')))
303			{
304				$content = array(
305					'account_id' => $account_id,
306					'contact_id' => (int)$_GET['contact_id'],
307				);
308			}
309			else
310			{
311				$content = array('account_id' => (int)$_GET['account_id']);
312			}
313			//error_log(__METHOD__."() \$_GET[account_id]=$_GET[account_id], \$_GET[contact_id]=$_GET[contact_id] content=".array2string($content));
314		}
315		if ($GLOBALS['egw']->acl->check('account_access',32,'admin') ||
316			$GLOBALS['egw_info']['user']['account_id'] == $content['account_id'])
317		{
318			Framework::window_close(lang('Permission denied!!!'));
319		}
320		if ($content['delete'])
321		{
322			$cmd = new admin_cmd_delete_account(array(
323				'account' => $content['account_id'],
324				'new_user' => $content['new_owner'],
325				'is_user' => $content['account_id'] > 0,
326				'change_apps' => $content['delete_apps']
327			) +  (array)$content['admin_cmd']);
328			$msg = $cmd->run();
329			if ($content['contact_id'])
330			{
331				Framework::refresh_opener($msg, 'addressbook', $content['contact_id'], 'delete');
332			}
333			else
334			{
335				Framework::refresh_opener($msg, 'admin', $content['account_id'], 'delete');
336			}
337			Framework::window_close();
338		}
339
340		$sel_options = array();
341		$preserve = $content;
342
343		// Get a count of entries owned by the user
344		$counts = $GLOBALS['egw']->accounts->get_account_entry_counts($content['account_id']);
345		foreach($counts as $app => $counts)
346		{
347			$entry = Api\Link::get_registry($app, 'entries');
348			if(!$entry)
349			{
350				$entry = lang('Entries');
351			}
352			if($counts['total'] && Api\Hooks::exists('deleteaccount', $app))
353			{
354				$content['delete_apps'][] = $app;
355				$sel_options['delete_apps'][] = array(
356					'value' => $app,
357					'label' => lang($app) . ': ' . $counts['total'] . ' '.$entry
358				);
359			}
360			else if ($counts['total'])
361			{
362				// These ones don't support the needed hook
363				$content['counts'][] = array(
364					'app' => $app,
365					'count' => $counts['total'] . ' '.$entry
366				);
367			}
368		}
369		// Add filemanager home directory in as special case, hook is in the API
370		if(Api\Vfs::file_exists('/home/'.$GLOBALS['egw']->accounts->id2name($content['account_id'])))
371		{
372			$app = 'filemanager';
373			$sel_options['delete_apps'][] = array(
374				'value' => $app,
375				'label' => lang($app) . ': /home'
376			);
377			$content['delete_apps'][] = $app;
378		}
379
380		$tpl = new Etemplate('admin.account.delete');
381		$tpl->exec('admin_account::delete', $content, $sel_options, array(), $preserve, 2);
382	}
383
384	/**
385	 * Delete a group via ajax
386	 *
387	 * @param int $account_id
388	 * @param String[] $data Optional data
389	 * @param string $etemplate_exec_id to check against CSRF
390	 */
391	public static function ajax_delete_group($account_id, $data, $etemplate_exec_id)
392	{
393		Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
394
395		$cmd = new admin_cmd_delete_account(Api\Accounts::id2name(Api\Accounts::id2name($account_id)), null, false, (array)$data['admin_cmd']);
396		$msg = $cmd->run();
397
398		Api\Json\Response::get()->call('egw.refresh', $msg, 'admin', $account_id, 'delete');
399	}
400
401	/**
402	 * Check entered data and return error-msg via json data or null
403	 *
404	 * @param array $data values for account_id and account_lid
405	 * @param string $changed name of addressbook widget triggering change eg. "email", "n_given" or "n_family"
406	 */
407	public static function ajax_check(array $data, $changed)
408	{
409		// warn if anonymous user is renamed, as it breaks eg. sharing and Collabora
410		if ($changed == 'account_lid' && Api\Accounts::id2name($data['account_id']) === 'anonymous' && $data['account_lid'] !== 'anonymous')
411		{
412			Api\Json\Response::get()->data(lang("Renaming user 'anonymous' will break file sharing and Collabora Online Office!"));
413			return;
414		}
415
416		// for 1. password field just check password complexity
417		if ($changed == 'account_passwd')
418		{
419			$data['account_fullname'] = $data['account_firstname'].' '.$data['account_lastname'];
420			if (($error = Api\Auth::crackcheck($data['account_passwd'], null, null, null, $data)))
421			{
422				$error .= "\n\n".lang('If you ignore that error as admin, you should check "%1"!', lang('Must change password upon next login'));
423			}
424			Api\Json\Response::get()->data($error);
425			return;
426		}
427		// generate default email address, but only for new Api\Accounts
428		if (!$data['account_id'] && in_array($changed, array('n_given', 'n_family', 'account_lid')))
429		{
430			$email = Api\Accounts::email($data['account_firstname'], $data['account_lastname'], $data['account_lid']);
431			if ($email && $email[0] != '@' && strpos($email, '@'))	// only add valid email addresses
432			{
433				Api\Json\Response::get()->assign('addressbook-edit_email', 'value', $email);
434			}
435		}
436
437		if (!$data['account_lid'] && !$data['account_id']) return;	// makes no sense to check before
438
439		// set home-directory when account_lid is entered, but only for new Api\Accounts
440		if ($changed == 'account_lid' && !$data['account_id'] &&
441			$GLOBALS['egw_info']['server']['ldap_extra_attributes'] &&
442			$GLOBALS['egw_info']['server']['ldap_account_home'])
443		{
444			Api\Json\Response::get()->assign('addressbook-edit_homedirectory', 'value',
445				$GLOBALS['egw_info']['server']['ldap_account_home'].'/'.preg_replace('/[^a-z0-9_.-]/i', '',
446					Api\Translation::to_ascii($data['account_lid'])));
447		}
448
449		// set dummy membership to get no error about no members yet
450		$data['account_memberships'] = array($data['account_primary_user'] = $GLOBALS['egw_info']['user']['account_primary_group']);
451
452		try {
453			$cmd = new admin_cmd_edit_user($data['account_id'], $data);
454			$cmd->run(null, false, false, true);
455		}
456		catch(Exception $e)
457		{
458			Api\Json\Response::get()->data($e->getMessage());
459		}
460	}
461}
462