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