<?php

/**
 * 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
 */

/**
 * cfUserProfileInterface Interface
 *
 * Interface for classes that extend the user profile.
 *
 * @package clearFusionCMS
 */
interface cfUserProfileInterface {
	/**
	 * Build the form elements for the profile extension.
	 *
	 * @param flForm $form The form to add the fields to and processing the request.
	 * @param int $where Where the profile is being used, one of cfUserProfile::ADMIN, USEREDIT, USEREDITINMGR or REGISTRATION.
	 * @param flUser|int $user The flUser Object or ID of the user or null if creating a new user.
	 */
	function addFields($form, $where, $user);

	/**
	 * Process the form elements for the profile extension.
	 *
	 * @param flForm $form The form to add the fields to and processing the request.
	 * @param int $where Where the profile is being used, one of cfUserProfile::ADMIN, USEREDIT, USEREDITINMGR or REGISTRATION.
	 * @param flUser|int $user The flUser Object or ID of the user or null if creating a new user.
	 */
	function process($form, $where, $user);
}

/**
 * cfUserProfile Class
 *
 * Class to build and operate the the form for editing user
 * profiles, including module extensions to the user profile.
 *
 * @package clearFusionCMS
 */
class cfUserProfile {
	/**
	 * Editing/creating the profile in the admin.
	 */
	const ADMIN = 1;

	/**
	 * User is editing their own profile.
	 */
	const USEREDIT = 2;

	/**
	 * User is registering their own profile.
	 */
	const REGISTRATION = 3;

	/**
	 * User is editing their own profile within the manager interface.
	 */
	const USEREDITINMGR = 4;

	/**
	 * Allow username to be changed.
	 */
	const ALLOWCHANGE = 1;

	/**
	 * Allow username to be changed if not set.
	 */
	const ALLOWIFNOTSET = 2;

	/**
	 * The database object.
	 *
	 * @var flDb
	 */
	protected $db;

	/**
	 * The user object.
	 *
	 * @var flUser
	 */
	protected $user;

	/**
	 * The user id.
	 *
	 * @var int
	 */
	protected $userId;

	/**
	 * The extension fields.
	 *
	 * @var array
	 */
	protected $exts = array();

	/**
	 * Where the profile is being used.
	 *
	 * @var int
	 */
	protected $where;

	/**
	 * If username changes are allowed.
	 *
	 * @var int
	 */
	protected $usernameChange;

	/**
	 * Flags if display name allowed.
	 *
	 * @var bool
	 */
	protected $allowDisplayName;

	/**
	 * Flags if with 2 Factor Authentication.
	 *
	 * @var bool
	 */
	protected $with2FA;

	/**
	 * Flags if with bio editing.
	 *
	 * @var bool
	 */
	protected $withBio;

	/**
	 * Construct the object.
	 *
	 * @param flDb $db The database object.
	 * @param flUser $user The user object or user id if admin editing the user rather than user self editing.
	 * @param int $where Where the profile is being used, one of cfUserProfile::ADMIN, USEREDIT or REGISTRATION.
	 * @param int $usernameChange Allows changing of the username, one of cfUserProfile::ALLOWCHANGE, ALLOWIFNOTSET
	 * @param bool $allowDisplayName Flags if the display name can be edited.
	 * @param bool $with2FA Flags if to display 2 Factor Authentication.
	 */
	function __construct($db, $user, $where, $usernameChange = 0, $allowDisplayName = false, $with2FA = false, $withBio = false) {
		$this->db = $db;
		$this->setUser($user);
		$this->where = $where;
		$this->usernameChange = $usernameChange;
		$this->allowDisplayName = $allowDisplayName;
		$this->with2FA = $with2FA;
		$this->withBio = $withBio;

		// Get the user profile extension class for installed modules
		$mm = new flModuleMgr($db);
		$installed = $mm->getInstalled('UserProfile');
		foreach($installed as $n => $v) {
			try {
				class_exists($class = "{$n}UserProfile");
				$this->exts[] = new $class();
			} catch(flExceptionLoadClass $e) {
			}
		}
	}

	/**
	 * Set the user.
	 *
	 * @param flUser $user The user object or user id if admin editing the user rather than user self editing.
	 * @return cfUserProfile This object.
	 */
	function setUser($user) {
		if($user instanceof flUser) {
			$this->user = $user;
			$this->userId = $user->getId();
		}
		else {
			$this->user = null;
			$this->userId = $user;
		}
		return $this;
	}

	/**
	 * Build and process the form.
	 *
	 * @param flFrom $form The form object to use.
	 */
	function process($form) {
		if($this->user === null)
			return;

		$group = array();
		if($this->usernameChange == self::ALLOWCHANGE || ($this->usernameChange == self::ALLOWIFNOTSET && !$this->user->usernameSet())) {
			$uname = new flFormElementText($form, 'uname', $this->user->usernameSet() ? $this->user->getUsername() : '');
			$uname->setLabel(__('Username', 'cf'))
				->setRequired()
				->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 _.', 'cf')))
				->addValidator(new cfValidatorUsername(null, $this->user->getUsername()));
			$group[] = $uname;
		}

		$email = new flFormElementText($form, 'email', $this->user->getEmail());
		$email->setLabel(__('Email Address', 'cf'))
				->setRequired()
				->addValidator(new flFormValidatorRequired())
				->addValidator(new flFormValidatorMaxLength(200))
				->addValidator(new flFormValidatorEmail())
				->addValidator(new cfValidatorUserEmail(null, $this->user->getEmail()))
				->setDisabled($this->user->getAuthService() != 'internal');

		if($this->allowDisplayName) {
			$displayName = new flFormElementText($form, 'displayname', $this->user->getDisplayName());
			$displayName->setLabel(__('Display Name', 'cf'))
				->addValidator(new flFormValidatorMaxLength(32));
			$group[] = $displayName;
		}

		if($this->withBio) {
			$bio = new flFormElementTextArea($form, 'bio', $this->user->getBio());
			$bio->setLabel(__('Bio', 'cfBackend'))
				->setRows(4)
				->addValidator(new flFormValidatorMaxLength(4096));
			$group[] = $bio;
		}

		$pwd = new flFormElementPassword($form, 'pwd');
		$pwd->setLabel(__('New Password', 'cf'))
			->setDescription(__('The password must be at least 8 characters long, leave blank to leave unchanged.', 'cf'))
			->addValidator(new flFormValidatorMinLength(8))
			->setDisabled($this->user->getAuthService() != 'internal')
			->hide($this->user->getAuthService() != 'internal');

		$pwd2 = new flFormElementPassword($form, 'pwd2');
		$pwd2->setLabel(__('Confirm Password', 'cf'))
			->addValidator(new flFormValidatorSameAs($pwd))
			->setDisabled($this->user->getAuthService() != 'internal')
			->hide($this->user->getAuthService() != 'internal');

		$tz = new flFormElementSelect($form, 'tz', $this->user->getTimezone(), flDateTzMenu::get());
		$tz->setLabel(__('Timezone', 'cf'));

		$form->groupElements('user', array_merge($group, array($email, $pwd, $pwd2, $tz)))
			->setGroupLabel('user', __('Account Details', 'cf'))
			->focusElement(isset($uname) ? $uname : $email);

		$authKey = $this->user->getAuthKey();
		if($this->with2FA) {
			$ag = array();
			if(empty($authKey)) {
				$enable2fa = new flFormElementSubmit($form, 'enable2fa');
				$enable2fa->setCaption(__('Enable', 'cf'))->setCSS('primary');
				$ag[] = $enable2fa;
			}
			else {
				$cfg = new flConfigDb('cfSystemSecurity', $this->db);

				$tfaSecret = new flFormElementText($form, 'twofasecret', $authKey);
				$tfaSecret->setLabel(__('Secret Key', 'cf'))->setDisabled(true)->setReadonly(true);
				$ag[] = $tfaSecret;

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

				$disable2fa = new flFormElementSubmit($form, 'disable2fa');
				$disable2fa->setCaption(__('Disable', 'cf'))->setCSS('primary');
				$ag[] = $disable2fa;

				$regen2fa = new flFormElementSubmit($form, 'regen2fa');
				$regen2fa->setCaption(__('Regenerate Secret Key', 'cf'))->setCSS('primary');
				$ag[] = $regen2fa;
			}

			$form->groupElements('user2fa', $ag)
				->setGroupLabel('user2fa', __('Google Authenticator Code', 'cf'));
		}

		$uMgr = clearFusionCMS::getUserMgr();
		$this->addFields($form);

		$submit = new flFormElementSubmit($form, 'update');
		$submit->setCaption(__('Update My Profile', 'cf'))->setCSS('primary');

		$form->groupElements('submit', array($submit), new flFormDecoratorList(new flFormDecoratorDl()));

		if($form->isSubmitted() && $form->isValid()) {

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

			$this->processFields($form);

			if($this->user->getEmail() != $email->getValue() && !$uMgr->setEmail($this->user->getId(), $email->getValue()))
				$email->forceError(__('Unable to set the requested email address it may already be in use.', 'cf'));

			// If username changed
			if(isset($uname) && $uname->getValue() != $this->user->getUsername())
				$uMgr->changeUsername($this->user->getId(), $uname->getValue());

			if(isset($displayName)) {
				$dn = trim($displayName->getCleanedValue());
				if(isset($uname) && $uname->getValue() == $dn)
					$dn = '';
				else if($this->user->getUsername() == $dn)
					$dn = '';
				$uInfo = $uMgr->getInfo($this->user->getId());
				if($uInfo['displayname'] != $dn && !$uMgr->changeDisplayName($this->user->getId(), $dn))
					$displayName->forceError(__('Unable to set the requested display name.', 'cf'));
				else
					clearFusionCMS::getCache()->flush();
			}

			if(isset($bio)) {
				$b = trim($bio->getCleanedValue());
				if($b != $uInfo['bio'] && !$uMgr->changeBio($this->user->getId(), $b))
					$bio->forceError(__('Unable to set the bio.', 'cf'));
				else
					clearFusionCMS::getCache()->flush();
			}

			// If password changed
			$p = $pwd->getValue();
			if(!empty($p)) {
				if($uMgr->setPassword($this->user->getId(), $p)) {

					// Log user out and back in
					$username = $this->user->getUsername();
					$remember = $this->user->isRememberMe();

					$this->user->logout();
					$this->user->authenticate($username, $p, $remember);
					clearFusionCMS::getCache()->flush();
				}
				else
					$pwd->forceError(__('Unable to set the new password.', 'cf'));
			}

			$uMgr->setTimezone($this->user->getId(), $tz->getValue());
		}
	}

	/**
	 * Add the fields to the form.
	 *
	 * @param flForm $form The form object.
	 */
	function addFields($form) {
		foreach($this->exts as $e)
			$e->addFields($form, $this->where, $this->user === null ? $this->userId : $this->user);
	}

	/**
	 * Process the fields in the form.
	 *
	 * @param flForm $form The form object.
	 */
	function processFields($form) {
		foreach($this->exts as $e)
			$e->process($form, $this->where, $this->user === null ? $this->userId : $this->user);
	}
}
