<?php
/**
 * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
 * @author Bart Visscher <bartv@thisnet.nl>
 * @author Björn Schießle <bjoern@schiessle.org>
 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
 * @author Lukas Reschke <lukas@statuscode.ch>
 * @author Morris Jobke <hey@morrisjobke.de>
 * @author Robin Appelman <icewind@owncloud.com>
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 * @author Tom Needham <tom@owncloud.com>
 * @author Victor Dubiniuk <dubiniuk@owncloud.com>
 * @author Vincent Petry <pvince81@owncloud.com>
 * @author Sujith Haridasan <sharidasan@owncloud.com>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */

namespace OC\User;

use OC\Files\Cache\Storage;
use OC\Files\ObjectStore\ObjectStoreStorage;
use OC\Files\View;
use OC\Group\Manager;
use OC\Hooks\Emitter;
use OC_Helper;
use OCP\Events\EventEmitterTrait;
use OCP\IAvatarManager;
use OCP\IImage;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IConfig;
use OCP\IUserBackend;
use OCP\IUserSession;
use OCP\User\NotPermittedActionException;
use OCP\PreConditionNotMetException;
use OCP\User\IChangePasswordBackend;
use OCP\User\UserExtendedAttributesEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\GenericEvent;

class User implements IUser {
	use EventEmitterTrait;

	/** @var Account */
	private $account;

	/** @var Emitter|Manager $emitter */
	private $emitter;

	/** @var \OCP\IConfig $config */
	private $config;

	/** @var IAvatarManager */
	private $avatarManager;

	/** @var IURLGenerator */
	private $urlGenerator;

	/** @var EventDispatcher */
	private $eventDispatcher;

	/** @var AccountMapper */
	private $mapper;

	/** @var \OC\Group\Manager  */
	private $groupManager;

	/** @var Session  */
	private $userSession;

	/** @var null|array */
	private $userExtendedAttributes = null;

	/**
	 * This flag is true by default. This flag when set to false
	 * would not allow the set operations in this class. Or in other
	 * words the update to the accounts table will be restricted through this flag.
	 * This flag is only modified inside getExtendedAttributes method.
	 * @var bool
	 */
	private $allowUserAccountUpdate = true;

	/**
	 * User constructor.
	 *
	 * @param Account $account
	 * @param AccountMapper $mapper
	 * @param null $emitter
	 * @param IConfig|null $config
	 * @param null $urlGenerator
	 * @param EventDispatcher|null $eventDispatcher
	 * @param Manager|null $groupManager
	 * @param Session|null $userSession
	 */
	public function __construct(
		Account $account,
		AccountMapper $mapper,
		$emitter = null,
		IConfig $config = null,
		$urlGenerator = null,
		EventDispatcher $eventDispatcher = null,
		\OC\Group\Manager $groupManager = null,
		Session $userSession = null
	) {
		$this->account = $account;
		$this->mapper = $mapper;
		$this->emitter = $emitter;
		if ($eventDispatcher === null) {
			$eventDispatcher = \OC::$server->getEventDispatcher();
		}
		$this->eventDispatcher = $eventDispatcher;
		if ($config === null) {
			$config = \OC::$server->getConfig();
		}
		$this->config = $config;
		$this->urlGenerator = $urlGenerator;
		if ($this->urlGenerator === null) {
			$this->urlGenerator = \OC::$server->getURLGenerator();
		}
		$this->groupManager = $groupManager;
		if ($this->groupManager === null) {
			$this->groupManager = \OC::$server->getGroupManager();
		}
		$this->userSession = $userSession;
		if ($this->userSession === null) {
			$this->userSession = \OC::$server->getUserSession();
		}
	}

	/**
	 * get the user id
	 *
	 * @return string
	 */
	public function getUID() {
		return $this->account->getUserId();
	}

	/**
	 * get the user name
	 * TODO move username to account table
	 *
	 * @return string
	 */
	public function getUserName() {
		$uid = $this->getUID();
		return $this->config->getUserValue($uid, 'core', 'username', $uid);
	}

	/**
	 * set the user name
	 * TODO move username to account table
	 *
	 * @param string $userName
	 * @throws NotPermittedActionException
	 */
	public function setUserName($userName) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}

		$currentUserName = $this->getUserName();
		if ($userName !== $currentUserName) {
			$uid = $this->getUID();
			try {
				$this->config->setUserValue($uid, 'core', 'username', $userName);
			} catch (PreConditionNotMetException $e) {
				// ignore, because precondition is empty
			}
		}
	}

	/**
	 * get the display name for the user, if no specific display name is set
	 * it will fall back to the user id
	 *
	 * @return string
	 */
	public function getDisplayName(): string {
		$displayName = $this->account->getDisplayName() ?? '';
		if ($displayName === '') {
			$displayName = $this->getUID();
		}
		return $displayName;
	}

	/**
	 * set the displayname for the user
	 *
	 * @param string $displayName
	 * @return bool
	 * @throws NotPermittedActionException
	 */
	public function setDisplayName($displayName) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}

		if (!$this->canChangeDisplayName()) {
			return false;
		}
		$displayName = \trim($displayName);
		if ($displayName === $this->account->getDisplayName()) {
			return false;
		}
		$this->account->setDisplayName($displayName);
		$this->mapper->update($this->account);

		$backend = $this->account->getBackendInstance();
		if ($backend->implementsActions(Backend::SET_DISPLAYNAME)) {
			/* @phan-suppress-next-line PhanUndeclaredMethod */
			$backend->setDisplayName($this->account->getUserId(), $displayName);
		}

		$this->triggerChange('displayName', $displayName);

		return true;
	}

	/**
	 * set the email address of the user
	 *
	 * @param string|null $mailAddress
	 * @return void
	 * @throws NotPermittedActionException
	 * @since 9.0.0
	 */
	public function setEMailAddress($mailAddress) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}

		$mailAddress = \trim($mailAddress);
		if ($mailAddress === $this->account->getEmail()) {
			return;
		}
		$this->account->setEmail($mailAddress);
		$this->mapper->update($this->account);
		$this->triggerChange('eMailAddress', $mailAddress);
	}

	/**
	 * returns the timestamp of the user's last login or 0 if the user did never
	 * login
	 *
	 * @return int
	 */
	public function getLastLogin() {
		return (int)$this->account->getLastLogin();
	}

	/**
	 * updates the timestamp of the most recent login of this user
	 *
	 * @return void
	 */
	public function updateLastLoginTimestamp() {
		$this->account->setLastLogin(\time());
		$this->mapper->update($this->account);
	}

	/**
	 * returns the timestamp of the user's creation time
	 *
	 * @return int
	 */
	public function getCreationTime() {
		return (int)$this->account->getCreationTime();
	}
 
	/**
	 * Delete the user
	 *
	 * @return bool
	 */
	public function delete(): bool {
		if ($this->emitter) {
			$this->emitter->emit('\OC\User', 'preDelete', [$this]);
		}
		// get the home now because it won't return it after user deletion
		$homePath = $this->getHome();

		// We have to delete the user from all groups
		foreach (\OC::$server->getGroupManager()->getUserGroups($this) as $group) {
			$group->removeUser($this);
		}
		// Delete the user's keys in preferences
		\OC::$server->getConfig()->deleteAllUserValues($this->getUID());

		// Delete all mount points for user
		\OC::$server->getUserStoragesService()->deleteAllMountsForUser($this);
		//Delete external storage or remove user from applicableUsers list
		\OC::$server->getGlobalStoragesService()->deleteAllForUser($this);

		$view = new View();
		$fileInfo = $view->getFileInfo("/");
		if ($fileInfo !== false) {
			$homeStorage = $fileInfo->getStorage();
			$isPrimaryObjectStore = $homeStorage->instanceOfStorage(ObjectStoreStorage::class);
			if ($isPrimaryObjectStore) {
				/** @var ObjectStoreStorage $homeStorage */
				/** @phan-suppress-next-line PhanUndeclaredMethod */
				$homeStorage->removeAllFilesForUser($this);
			}
		}

		// Delete user files in /data/
		\OC_Helper::rmdirr($homePath);

		// Delete the users entry in the storage table
		Storage::remove('home::' . $this->getUID());

		\OC::$server->getCommentsManager()->deleteReferencesOfActor('users', $this->getUID());
		\OC::$server->getCommentsManager()->deleteReadMarksFromUser($this);

		# finally delete the user and account records
		# this way the operation can be re-run in case of errors
		$this->mapper->delete($this->account);
		$bi = $this->account->getBackendInstance();
		if ($bi !== null) {
			$bi->deleteUser($this->account->getUserId());
		}

		if ($this->emitter) {
			$this->emitter->emit('\OC\User', 'postDelete', [$this]);
		}
		return true;
	}

	/**
	 * Set the user's password
	 *
	 * @param string $password
	 * @param string $recoveryPassword for the encryption app to reset encryption keys
	 * @return bool
	 * @throws \InvalidArgumentException
	 * @throws NotPermittedActionException
	 */
	public function setPassword($password, $recoveryPassword = null) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}

		if (\OCP\Util::isEmptyString($password)) {
			throw new \InvalidArgumentException('Password cannot be empty');
		}

		return $this->emittingCall(function () use (&$password, &$recoveryPassword) {
			if ($this->emitter) {
				$this->emitter->emit('\OC\User', 'preSetPassword', [$this, $password, $recoveryPassword]);
				\OC::$server->getEventDispatcher()->dispatch(
					new GenericEvent(null, ['uid'=> $this->getUID(), 'password' => $password]),
					'OCP\User::validatePassword'
				);
			}
			if ($this->canChangePassword()) {
				/** @var IChangePasswordBackend $backend */
				$backend = $this->account->getBackendInstance();
				'@phan-var IChangePasswordBackend $backend';
				$result = $backend->setPassword($this->getUID(), $password);
				if ($result) {
					if ($this->emitter) {
						$this->emitter->emit('\OC\User', 'postSetPassword', [$this, $password, $recoveryPassword]);
					}
					$this->config->deleteUserValue($this->getUID(), 'owncloud', 'lostpassword');
				}
				return !($result === false);
			} else {
				return false;
			}
		}, [
			'before' => ['user' => $this, 'password' => $password, 'recoveryPassword' => $recoveryPassword],
			'after' => ['user' => $this, 'password' => $password, 'recoveryPassword' => $recoveryPassword]
		], 'user', 'setpassword');
	}

	/**
	 * get the users home folder to mount
	 *
	 * @return string
	 */
	public function getHome() {
		return $this->account->getHome();
	}

	public function setHome(string $newLocation) {
		$this->account->setHome($newLocation);
	}

	/**
	 * Get the name of the backend class the user is connected with
	 *
	 * @return string
	 */
	public function getBackendClassName() {
		$b = $this->account->getBackendInstance();
		if ($b instanceof IUserBackend) {
			return $b->getBackendName();
		}
		return $this->account->getBackend();
	}

	/**
	 * check if the backend allows the user to change his avatar on Personal page
	 *
	 * @return bool
	 */
	public function canChangeAvatar() {
		$backend = $this->account->getBackendInstance();
		if ($backend === null) {
			return false;
		}
		if ($backend->implementsActions(Backend::PROVIDE_AVATAR)) {
			/* @phan-suppress-next-line PhanUndeclaredMethod */
			return $backend->canChangeAvatar($this->getUID());
		}
		return true;
	}

	/**
	 * check if the backend supports changing passwords
	 *
	 * @return bool
	 */
	public function canChangePassword() {
		$backend = $this->account->getBackendInstance();
		if ($backend === null) {
			return false;
		}
		return $backend instanceof IChangePasswordBackend || $backend->implementsActions(Backend::SET_PASSWORD);
	}

	/**
	 * check if the backend supports changing display names
	 *
	 * @return bool
	 */
	public function canChangeDisplayName() {
		if ($this->userSession instanceof IUserSession) {
			$user = $this->userSession->getUser();
			if (
				($this->config->getSystemValue('allow_user_to_change_display_name') === false) &&
				(($user !== null) && (!$this->groupManager->isAdmin($user->getUID()))) &&
				(($user !== null) && (!$this->groupManager->getSubAdmin()->isSubAdmin($user)))
			) {
				return false;
			}
		}

		$backend = $this->account->getBackendInstance();
		if ($backend === null) {
			return false;
		}
		return $backend->implementsActions(Backend::SET_DISPLAYNAME);
	}

	/**
	 * check if the backend supports changing email addresses
	 *
	 * @return bool
	 */
	public function canChangeMailAddress() {
		if ($this->userSession instanceof IUserSession) {
			$user = $this->userSession->getUser();
			if (
				($this->config->getSystemValue('allow_user_to_change_mail_address') === false) &&
				(($user !== null) && (!$this->groupManager->isAdmin($user->getUID()))) &&
				(($user !== null) && (!$this->groupManager->getSubAdmin()->isSubAdmin($user)))
			) {
				return false;
			}
		}

		return true;
	}

	/**
	 * check if the user is enabled
	 *
	 * @return bool
	 */
	public function isEnabled() {
		return $this->account->getState() === Account::STATE_ENABLED;
	}

	/**
	 * Set the enabled status for the user
	 *
	 * @param bool $enabled
	 * @throws NotPermittedActionException
	 */
	public function setEnabled($enabled) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}

		if ($enabled === true) {
			$this->account->setState(Account::STATE_ENABLED);
		} else {
			$this->account->setState(Account::STATE_DISABLED);
		}
		$this->mapper->update($this->account);

		if ($this->eventDispatcher) {
			$this->eventDispatcher->dispatch(new GenericEvent($this), self::class . '::postSetEnabled');
		}
	}

	/**
	 * get the users email address
	 *
	 * @return string|null
	 * @since 9.0.0
	 */
	public function getEMailAddress() {
		return $this->account->getEmail();
	}

	/**
	 * get the users' quota
	 *
	 * @return string
	 * @since 9.0.0
	 */
	public function getQuota() {
		$quota = $this->account->getQuota();
		if ($quota === null) {
			return 'default';
		}
		return $quota;
	}

	/**
	 * Set the users' quota
	 *
	 * @param string $quota
	 * @return void
	 * @throws NotPermittedActionException
	 * @throws PreConditionNotMetException
	 * @since 9.0.0
	 */
	public function setQuota($quota) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}

		if ($quota !== 'none' and $quota !== 'default') {
			$quota = OC_Helper::computerFileSize($quota);
			$quota = OC_Helper::humanFileSize($quota);
		}
		$this->account->setQuota($quota);
		// Set the quota on the preferences table as an override
		$this->config->setUserValue($this->getUID(), 'files', 'quota', $quota);
		$this->mapper->update($this->account);
		$this->triggerChange('quota', $quota);
	}

	/**
	 * get the avatar image if it exists
	 *
	 * @param int $size
	 * @return IImage|null
	 * @since 9.0.0
	 */
	public function getAvatarImage($size) {
		// delay the initialization
		if ($this->avatarManager === null) {
			$this->avatarManager = \OC::$server->getAvatarManager();
		}

		$avatar = $this->avatarManager->getAvatar($this->getUID());
		$image = $avatar->get($size);
		if ($image) {
			return $image;
		}

		return null;
	}

	/**
	 * get the federation cloud id
	 *
	 * @return string
	 * @since 9.0.0
	 */
	public function getCloudId() {
		$uid = $this->getUID();
		$server = $this->urlGenerator->getAbsoluteURL('/');
		return $uid . '@' . \rtrim($this->removeProtocolFromUrl($server), '/');
	}

	/**
	 * @param string $url
	 * @return string
	 */
	private function removeProtocolFromUrl($url) {
		if (\strpos($url, 'https://') === 0) {
			return \substr($url, \strlen('https://'));
		} elseif (\strpos($url, 'http://') === 0) {
			return \substr($url, \strlen('http://'));
		}

		return $url;
	}

	public function triggerChange($feature, $value = null) {
		if ($this->emitter && \in_array($feature, $this->account->getUpdatedFields())) {
			$this->emitter->emit('\OC\User', 'changeUser', [$this, $feature, $value]);
		}
	}

	/**
	 * @return string[]
	 * @since 10.0.1
	 */
	public function getSearchTerms() {
		$terms = [];
		foreach ($this->mapper->findByAccountId($this->account->getId()) as $term) {
			$terms[] = $term->getTerm();
		}
		return $terms;
	}

	/**
	 * @param string[] $terms
	 * @throws NotPermittedActionException
	 * @since 10.0.1
	 */
	public function setSearchTerms(array $terms) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}

		// Check length of terms, cut if too long
		$terms = \array_map(function ($term) {
			return \substr($term, 0, 191);
		}, $terms);
		$this->mapper->setTermsForAccount($this->account->getId(), $terms);
	}

	/**
	 * @return integer
	 * @since 11.0.0
	 */
	public function getAccountId() {
		return $this->account->getId();
	}

	/**
	 * get the attributes of user for apps
	 * This method sends event which is listened by the apps. The apps would add attributes which
	 * are specific to this user. Say for example a user might have access to a blog site, in such
	 * case the app which is responsible for this control could listen to this event and
	 * add an attribute say:
	 * "blogSite" => "https://foo/bar"
	 * Apps add attributes and their value in the form of key => value. The userExtendedAttributes
	 * does not care which app added the attributes. It only considers about the
	 * attributes.
	 * The argument clearCache is used to clear userExtendedAttributes. If there are
	 * external apps involved or under any circumstance we know there will be delay
	 * in response from the app, then its safe to use clearCache as false.
	 * New event is triggered under the following conditions:
	 * - if the userExtendedAttributes is null or empty array ( even if clearCache is set to false, in this condition, event will be triggered )
	 * - if clearCache is set to true
	 * The flag allowUserAccountUpdate is set to true by default. This flag is set to false before event is emitted.
	 * This flag is checked on all the set operations, in this class to make sure no user account
	 * table update is allowed when the extended attributes are provided by the apps. Once the
	 * event listeners have done their task, the flag is set back to true.
	 * The exception is thrown when the listener tries call this method again. This is to
	 * prevent infinite loop. Also the exceptions thrown during any operation by the listeners
	 * are allowed to go up. The exceptions are not caught in this method.
	 *
	 *
	 * @param bool $clearCache set to true if user attributes should be created every time, else false is set to reuse the userExtendedAttributes cache.
	 * @return array
	 * @throws NotPermittedActionException
	 * @since 10.11.0
	 */
	public function getExtendedAttributes($clearCache = false) {
		if (!$this->allowUserAccountUpdate) {
			throw new NotPermittedActionException("Operation cannot be allowed as other apps are fetching extended attributes of this user.");
		}
		/**
		 * using empty because userExtendedAttributes could either be null or empty array
		 * or an array of attributes
		 */
		if ($clearCache || !isset($this->userExtendedAttributes) || $this->userExtendedAttributes === []) {
			$userExtendedAttributesEvent = new UserExtendedAttributesEvent($this);

			/**
			 * Restrict the user accounts table update for the set operations as the apps
			 * listening to the event below will be providing extended attributes of this
			 * user.
			 */
			$this->allowUserAccountUpdate = false;
			try {
				$this->eventDispatcher->dispatch($userExtendedAttributesEvent, UserExtendedAttributesEvent::USER_EXTENDED_ATTRIBUTES);
			} finally {
				//Reset the flag to true so that account table can now be allowed to update.
				$this->allowUserAccountUpdate = true;
			}

			//Just overwrite the userExtendedAttributes if clearCache is true
			$this->userExtendedAttributes = $userExtendedAttributesEvent->getAttributes();
		}

		return $this->userExtendedAttributes;
	}
}
