lib/private/User/User.php
<?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() {
$displayName = $this->account->getDisplayName();
if (\strlen($displayName) === 0) {
$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;
}
}