<?php

/**
 * Class file for user management action.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package clearFusionCMS
 * @copyright Copyright (c) 2012 - 2016 clearFusionCMS. All rights reserved.
 * @link http://clearfusioncms.com
 */

/**
 * cfBackendEdituserAction Class
 *
 * Action class to edit an existing user.
 *
 * @package clearFusionCMS
 */
class cfBackendEdituserAction extends cfBackendControllerAction {
	/**
	 * Process the action.
	 */
	function process() {
		$this->testPermission('core.users');
		$this->breadcrumbTrail->addCrumb(__('Users', 'cfBackend'), $this->assemble('', 'listusers'));
		$this->breadcrumbTrail->addCrumb(__('Edit User', 'cfBackend'), $this->assemble('', 'edituser'));

		$acMgr = new flAcMgr($this->db);
		$uMgr = clearFusionCMS::getUserMgr();

		// Get the user id to edit
		$uid = $this->request->getParam('*');
		$delUid = 0;
		if($uid !== null && count($uid) == 1 && preg_match('/^\d+$/', $uid[0]))
			$uid = $uid[0];
		else if($uid !== null && count($uid) == 2 && preg_match('/^\d+$/', $uid[0]) && $uid[1] == 'delete') {
			$delUid = $uid[0];
			$uid = 0;
		}
		else
			$uid = 0;

		$isAdmin = array_key_exists(1, $uMgr->getUserRoles($this->user->getId()));
		$isTgtAdmin = array_key_exists(1, $rls = $uMgr->getUserRoles($uid ? $uid : $delUid));

		$msg = $this->session->flashGet('cfManagerOpSuccess', '');
		if($delUid) {
			$this->testPermission('core.usersdel');
			if($delUid == $this->user->getId() || (!$isAdmin && $isTgtAdmin))
				flApp::getResponse()->redirectTo($this->assemble('', 'dashboard', array('nopermission'), true));

			if(($content = $this->lockObject('cfUserEdit', $delUid, true)) === true) {
				$userInfo = $uMgr->getInfo($delUid);

				$form = new flForm($this->request, 'deluserform', $this->assemble('', 'edituser', array($delUid, 'delete')), true);

				$username = new flFormElementText($form, 'username', $userInfo['username']);
				$username->setLabel(__('Username', 'cfBackend'))
					->setReadonly()
					->setDisabled();

				$delSubmit = new flFormElementSubmit($form, 'deleteuser');
				$delSubmit->setCaption(__('Permanently Delete User', 'cfBackend'))
					->setCSS('danger')
					->setDescription($this->alertMessage('Warning', __('This action cannot be undone and will delete the user and all of their data.', 'cfBackend')));

				if($form->isSubmitted() && $form->isValid()) {
					$roleList = $acMgr->getRoles();

					$prevRoles = [];
					foreach($rls as $k => $v) {
						$prevRoles[] = $roleList[$k];
					}

					$prev = $userInfo;
					unset($prev['authcode'], $prev['authkey'], $prev['token']);
					$prev['using2FA'] = !empty($userInfo['authkey']);
					$prev['roles'] = $prevRoles;
					$prev['blockInfo'] = $uMgr->getBlockInfo($delUid);

					cfUserAudit::deleteUser($delUid, $userInfo['username'], $prev);

					if($delUid != $this->user->getId())
						$uMgr->delete($delUid);
					$this->response->redirectTo($this->assemble('', 'listusers', array(), true));
				}

				$content = $this->renderBlock(__('Delete User', 'cfBackend'), $form->render($this->tpl->getCodeBuffer()));
			}
		}
		else {
			if(!$uid || !($userInfo = $uMgr->getInfo($uid)))
				$this->response->redirectTo($this->assemble('', 'listusers', array(), true));

			$isExternal = $userInfo['service'] != 'internal';

			if(($content = $this->lockObject('cfUserEdit', $uid, true)) === true) {
				$form = new flForm($this->request, 'adduserform', $this->assemble('', 'edituser', array($uid)), true);

				$username = new flFormElementText($form, 'username', $userInfo['username']);
				$username->setLabel(__('Username', 'cfBackend'))
						->setDescription(__('The username for the user.', 'cfBackend'))
						->setRequired()
						->autocompleteOff()
						->addValidator(new flFormValidatorRequired())
						->addValidator(new flFormValidatorMinLength(3))
						->addValidator(new flFormValidatorMaxLength(100))
						->addValidator(new flFormValidatorRegex('/^[0-9a-zA-Z._-]+$/', __('The username can only contain numbers, letters and the characters ., - and _.', 'cfBackend')))
						->addValidator(new cfValidatorUsername(null, $userInfo['username']));

				$displayName = new flFormElementText($form, 'displayname', $userInfo['displayname']);
				$displayName->setLabel(__('Display Name', 'cfBackend'))
						->setDescription(__('Optional display name for the user.', 'cfBackend'))
						->addValidator(new flFormValidatorMaxLength(32));

				$email = new flFormElementText($form, 'email', $userInfo['email']);
				$email->setLabel(__('Email', 'cfBackend'))
						->setDescription(__('The email address for the user.', 'cfBackend'))
						->setRequired()
						->addValidator(new flFormValidatorRequired())
						->addValidator(new flFormValidatorMaxLength(200))
						->addValidator(new flFormValidatorEmail())
						->addValidator(new cfValidatorUserEmail(null, $userInfo['email']))
						->setDisabled($isExternal);

				$tz = new flFormElementSelect($form, 'tz', $userInfo['tz'], flDateTzMenu::get());
				$tz->setLabel(__('Timezone', 'cfBackend'));

				$password = new flFormElementPassword($form, 'password', '');
				$password->setLabel(__('Password', 'cfBackend'))
					->setDescription(__('The password must be at least 8 characters long. Leave blank to leave password unchanged.', 'cfBackend'))
					->addValidator(new flFormValidatorMinLength(8))
					->setDisabled($isExternal)
					->hide($isExternal);

				$bio = new flFormElementTextArea($form, 'bio', $userInfo['bio']);
				$bio->setLabel(__('Bio', 'cfBackend'))
					->setRows(4)
					->addValidator(new flFormValidatorMaxLength(4096));

				$form->groupElements('user', array($username, $displayName, $bio, $email, $password, $tz))
					->setGroupLabel('user', __('Account Details', 'cfBackend'));

				$cfg = new flConfigDb('cfSystemSecurity', $this->db);
				if($cfg->get('twoFactor', false)) {
					if(empty($userInfo['authkey'])) {
						$enable2fa = new flFormElementSubmit($form, 'enable2fa');
						$enable2fa->setCaption(__('Enable', 'cfBackend'))->setCSS('primary');

						$form->groupElements('twofa', array($enable2fa))
							->setGroupLabel('twofa', __('Two Factor Authentication', 'cfBackend'));
					}
					else {
						$tfaName = new flFormElementText($form, 'twofaname', $cfg->get('twoFactorName', clearFusionCMS::getSiteName()));
						$tfaName->setLabel(__('Key Name', 'cfBackend'))
							->setDisabled(true)
							->setReadonly(true);

						$tfaSecret = new flFormElementText($form, 'twofasecret', $userInfo['authkey']);
						$tfaSecret->setLabel(__('Secret Key', 'cfBackend'))
							->setDisabled(true)
							->setReadonly(true);

						$qr = new flFormElementPane($form, 'twofaqr', '<img src="' . flAuthTOTP::getQRCode($cfg->get('twoFactorName', clearFusionCMS::getSiteName()), $userInfo['authkey'], 200) . '" alt="QR" class="qrcode2fa" />');

						$disable2fa = new flFormElementSubmit($form, 'disable2fa');
						$disable2fa->setCaption(__('Disable', 'cfBackend'))->setCSS('primary');

						$regen2fa = new flFormElementSubmit($form, 'regen2fa');
						$regen2fa->setCaption(__('Regenerate Secret Key', 'cfBackend'))->setCSS('primary');

						$form->groupElements('twofa', array($tfaName, $tfaSecret, $qr, $disable2fa, $regen2fa))
							->setGroupLabel('twofa', __('Two Factor Authentication', 'cfBackend'));
					}
				}

				$userProfile = new cfUserProfile($this->db, $uid, cfUserProfile::ADMIN);
				$userProfile->addFields($form, array());

				$state = new flFormElementSelect($form, 'userstate', $uMgr->getState($uid), array('active' => __('Active', 'cfBackend'), 'disabled' => __('Disabled', 'cfBackend'), 'pending' => __('Pending', 'cfBackend'), 'blocked' => __('Blocked', 'cfBackend')));
				$state->setLabel(__('Account State', 'cfBackend'));

				if($blockInfo = $uMgr->getBlockInfo($uid)) {
					$date = new flDate($blockInfo['blockliftat'], $this->user->getTimezone());
					$date = ' (' . $date->toString() . ')';
				}
				else
					$date = '';

				$blockfor = new flFormElementSelect($form, 'blockfor', $blockInfo === null ? 0 : $blockInfo['blockeddays'], array(
					0 => __('Forever', 'cfBackend'),
					1 => __('1 Day', 'cfBackend') . $date,
					2 => __('2 Days', 'cfBackend') . $date,
					3 => __('3 Days', 'cfBackend') . $date,
					4 => __('4 Days', 'cfBackend') . $date,
					5 => __('5 Days', 'cfBackend') . $date,
					6 => __('6 Days', 'cfBackend') . $date,
					7 => __('1 Week', 'cfBackend') . $date,
					14 => __('2 Weeks', 'cfBackend') . $date,
					21 => __('3 Weeks', 'cfBackend') . $date,
					30 => __('1 Month', 'cfBackend') . $date,
					60 => __('2 Months', 'cfBackend') . $date,
					90 => __('3 Months', 'cfBackend') . $date,
					120 => __('4 Months', 'cfBackend') . $date,
					150 => __('5 Months', 'cfBackend') . $date,
					180 => __('6 Months', 'cfBackend') . $date,
					210 => __('7 Months', 'cfBackend') . $date,
					240 => __('8 Months', 'cfBackend') . $date,
					270 => __('9 Months', 'cfBackend') . $date,
					300 => __('10 Months', 'cfBackend') . $date,
					330 => __('11 Months', 'cfBackend') . $date,
					365 => __('1 Year', 'cfBackend') . $date
				));
				$blockfor->setLabel(__('Block For', 'cfBackend'))
					->setDescription(__('The time to block the user for from now.', 'cfBackend'));

				$blockedreason = new flFormElementText($form, 'blockedreason', $blockInfo === null ? null : $blockInfo['blockreason']);
				$blockedreason->setLabel(__('Blocked Reason', 'cfBackend'))
					->addValidator(new flFormValidatorMaxLength(255));

				if($blockInfo) {
					if($blockingUser = $uMgr->getInfo($blockInfo['blockedby']))
						$blockedreason->setDescription(sprintf(__('Blocked by %s', 'cfBackend'), $blockingUser['username']));

					// If blocked then don't allow changes
					$blockfor->setReadonly()->setDisabled();
					$blockedreason->setReadonly()->setDisabled();
				}

				// Don't allow self editing of roles unless admin, don't allow non-admin to edit admin
				if($this->user->hasPermission('core.usersroles')) {
					if(($uid != $this->user->getId() || $isAdmin) && ($isAdmin || !$isTgtAdmin)) {
						$roles = new flFormElementChecklist($form, 'roles', array_keys($rls), $roleList = $acMgr->getRoles());
						$roles->setLabel(__('Assign User Roles', 'cfBackend'));
						if($uMgr->remoteAuth())
							$roles->setDescription($this->alertMessage('Info', __('This system is using remote authentication therefore roles not available on the master will be ignored.', 'cfBackend')));
					}
				}

				$date = new flDate($userInfo['added'] . ' UTC', $this->user->getTimezone());
				$addedat = new flFormElementText($form, 'addedat', $date->toString());
				$addedat->setLabel(__('Added At', 'cfBackend'))
					->setReadonly()
					->setDisabled();

				if($userInfo['lastaccess'] != null) {
					$date = new flDate();
					$ago = $date->timeSpan(new flDate($userInfo['lastaccess']));
					$ago = sprintf(__('%1$d %2$s Ago', 'cfBackend'), $ago['offset'], ucfirst($ago['units']));
				}
				else
					$ago = __('Never', 'cfBackend');

				$lastaccess = new flFormElementText($form, 'lastaccess', $ago);
				$lastaccess->setLabel(__('Last Access', 'cfBackend'))
					->setReadonly()
					->setDisabled();

				$elems = array();
				$submit = new flFormElementSubmit($form, 'edituser');
				$submit->setCaption(__('Update User', 'cfBackend'))->setCSS('primary');
				$elems[] = $submit;

				$logoutUser = new flFormElementSubmit($form, 'logoutuser');
				$logoutUser->setCaption(__('Logout User', 'cfBackend'))
					->setCSS('danger');
				$elems[] = $logoutUser;

				if($uid != $this->user->getId() && $this->user->hasPermission('core.usersdel') && ($isAdmin || !$isTgtAdmin)) {
					$delSubmit = new flFormElementSubmit($form, 'deleteuser');
					$delSubmit->setCaption(__('Delete User', 'cfBackend'))
						->setCSS('danger');
					$elems[] = $delSubmit;
				}

				$form->groupElements('state', array($state, $blockfor, $blockedreason))
						->setGroupLabel('state', __('Account State', 'cfBackend'));
				if(isset($roles)) {
					$form->groupElements('roles', array($roles))
						->setGroupLabel('roles', __('Role Assignment', 'cfBackend'));
				}
				$form->groupElements('userinfo', array($addedat, $lastaccess))
					->setGroupLabel('userinfo', __('User Info', 'cfBackend'))
					->groupElements('submit', $elems, new flFormDecoratorList(new flFormDecoratorDl()));

				$form->focusElement($username);

				if($form->isSubmitted() && $form->isValid()) {
					$done = true;

					// If enabling 2fa
					if((isset($enable2fa) && $enable2fa->getValue()) || (isset($regen2fa) && $regen2fa->getValue()))
						$uMgr->setAuthKey($uid, flAuthTOTP::generateSecret());
					else if(isset($disable2fa) && $disable2fa->getValue())
						$uMgr->setAuthKey($uid, '');

					$profile = array();
					$userProfile->processFields($form, $profile);

					// If username changed
					if($username->getValue() != $userInfo['username']) {
						if(!$uMgr->changeUsername($uid, $username->getValue())) {
							$username->forceError(__('Unable to change the username.', 'cfBackend'));
							$done = false;
						}
						else
							$this->cache->flush();
					}

					// If email changed
					if($done && $email->getValue() != $userInfo['email']) {
						if(!$uMgr->setEmail($uid, $email->getValue())) {
							$email->forceError(__('Unable to change the email address.', 'cfBackend'));
							$done = false;
						}
					}

					// If display name changed
					$dn = trim($displayName->getCleanedValue());
					if($dn == $username->getValue())
						$dn = '';
					if($done && $userInfo['displayname'] != $dn) {
						if(!$uMgr->changeDisplayName($uid, $dn)) {
							$email->forceError(__('Unable to change the users display name.', 'cfBackend'));
							$done = false;
						}
						else {
							$displayName->setValue($dn);
							$this->cache->flush();
						}
					}

					// If bio changed
					$bioTxt = trim($bio->getCleanedValue());
					if($done && $userInfo['bio'] != $bioTxt) {
						if(!$uMgr->changeBio($uid, $bioTxt)) {
							$bio->forceError(__('Unable to set the bio.', 'cfBackend'));
							$done = false;
						}
						else {
							$bio->setValue($bioTxt);
							$this->cache->flush();
						}
					}

					// If password given
					$pwd = $password->getValue();
					if($done && !empty($pwd))
						$uMgr->setPassword($uid, $password->getValue());

					if($done) {
						if($state->getValue() == 'blocked') {
							$uMgr->block($uid, $this->user->getId(), $blockfor->getValue(), $blockedreason->getValue());
							$blockfor->setReadonly()->setDisabled();
							$blockedreason->setReadonly()->setDisabled();
						}
						else {
							$uMgr->setState($uid, $state->getValue());
							$blockfor->setReadonly(false)->setDisabled(false);
							$blockedreason->setReadonly(false)->setDisabled(false)->setValue('');
						}

						// Update the roles
						$auditRoles = [];
						$prevRoles = [];
						if(isset($roles)) {
							$uMgr->setUserRoles($uid, $roles->getValue());
							$roles->setValue(array_keys($uMgr->getUserRoles($uid)));

							foreach($roles->getValue() as $v) {
								$auditRoles[] = $roleList[$v];
							}
							foreach($rls as $k => $v) {
								$prevRoles[] = $roleList[$k];
							}
						}
						$uMgr->setTimezone($uid, $tz->getValue());

						$prev = $userInfo;
						unset($prev['authcode'], $prev['authkey'], $prev['token']);
						$prev['using2FA'] = !empty($userInfo['authkey']);
						$prev['roles'] = $prevRoles;
						$prev['blockInfo'] = $blockInfo;
						cfUserAudit::updateUser($uid, $username->getValue(), $auditRoles, $prev);

						$msg = $this->alertMessage('Success', __('The user has been updated.', 'cfBackend'));
					}

					// If logout user
					if($logoutUser->getValue()) {
						$uMgr->forceLogout($uid);
						$msg = $this->alertMessage('Success', __('The user has been logged out of all computers.', 'cfBackend'));
					}

					// If deleting
					if(isset($delSubmit) && $delSubmit->getValue())
						$this->response->redirectTo($this->assemble('', 'edituser', array($uid, 'delete'), true));

					if((isset($enable2fa) && $enable2fa->getValue()) ||
						(isset($disable2fa) && $disable2fa->getValue()) ||
						(isset($regen2fa) && $regen2fa->getValue())
					) {
						$this->session->flashSet('cfManagerOpSuccess', $msg);
						$this->response->redirectTo($this->request->getSelf(true));
					}
				}

				$content = $this->renderBlock(__('Edit User', 'cfBackend'),  $form->render($this->tpl->getCodeBuffer()), __('Update the form below to update the user.', 'cfBackend'), $msg);
			}
		}

		// Build the search form
		$data = $this->session->flashGet('cfManager.user.search');
		$this->session->flashKeep('cfManager.user.search');

		$this->confirmLeavePage();
		$form = new flForm($this->request, 'searchform', $this->assemble('', 'listusers'), true);

		$searchfor = new flFormElementText($form, 'searchfor', $data === null ? null : $data['for']);
		$searchfor->setLabel(__('Search For', 'cfBackend'));

		$searchin = new flFormElementChecklist($form, 'searchin', $data === null ? array('username', 'email') : $data['in'], array(
			'username' => __('Username', 'cfBackend'),
			'email' => __('Email', 'cfBackend')));
		$searchin->setLabel(__('Search In', 'cfBackend'))
			->addValidator(new flFormValidatorMinChecklist(1));

		$ac = new flAcMgr($this->db);
		$roleLim = new flFormElementSelect($form, 'rolelim', $data === null ? '' : $data['rolelim'], array(0 => __('[All Roles]', 'cfBackend')) + $ac->getRoles());
		$roleLim->setLabel(__('Limit to Role', 'cfBackend'));

		$exact = new flFormElementCheckbox($form, 'exact', $data === null ? 0 : $data['exact']);
		$exact->setLabel(__('Exact Match', 'cfBackend'));

		$submit = new flFormElementSubmit($form, 'Search');
		$submit->setCaption(__('Search', 'cfBackend'))->setCSS('primary');

		// Build floating action menu
		$fam = new flWidgetFloatingActionMenu();
		$fam->setCSS('[[+css]]');

		if($this->user->hasPermission('core.usersadd'))
			$fam->addAction('person_add', __('Add User', 'cfBackend'), $this->assemble('', 'adduser'));
		$fam->addAction('lock_open', __('Clear IP Blocks', 'cfBackend'), $this->assemble('', 'clearipblocks'));

		$this->tpl->registerChunk('fam', $fam->render());

		$this->renderPage(__('Edit User', 'cfBackend'),
			$content,
			$this->renderBlock('', $form->render($this->tpl->getCodeBuffer())));
	}
}
