Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
31 / 31 |
CRAP | |
100.00% |
204 / 204 |
AclUser\Service\UserManager | |
100.00% |
1 / 1 |
|
100.00% |
31 / 31 |
66 | |
100.00% |
204 / 204 |
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
giveUserBasicRole | |
100.00% |
1 / 1 |
1 | |
100.00% |
8 / 8 |
|||
updateUser | |
100.00% |
1 / 1 |
3 | |
100.00% |
7 / 7 |
|||
checkUserExists | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
getUserByEmailAddress | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
getUserById | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
validatePassword | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
generatePasswordResetToken | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
changePassword | |
100.00% |
1 / 1 |
4 | |
100.00% |
8 / 8 |
|||
fetchAllRoles | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
getRoleByName | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
getRolesByUserId | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
validateForgottenPasswordForm | |
100.00% |
1 / 1 |
3 | |
100.00% |
15 / 15 |
|||
validateRegistrationForm | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
createNewUser | |
100.00% |
1 / 1 |
4 | |
100.00% |
15 / 15 |
|||
sendConfirmNewAccountEmail | |
100.00% |
1 / 1 |
1 | |
100.00% |
9 / 9 |
|||
sendForgottenPasswordEmail | |
100.00% |
1 / 1 |
1 | |
100.00% |
9 / 9 |
|||
getUserPhotoLocationById | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
validateChangePasswordForm | |
100.00% |
1 / 1 |
4 | |
100.00% |
11 / 11 |
|||
checkResetToken | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
validateResetPasswordForm | |
100.00% |
1 / 1 |
4 | |
100.00% |
16 / 16 |
|||
checkPasswordResetTokenForUser | |
100.00% |
1 / 1 |
3 | |
100.00% |
7 / 7 |
|||
getUserByPasswordResetToken | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
updateValidUsersPassword | |
100.00% |
1 / 1 |
2 | |
100.00% |
7 / 7 |
|||
activateAccountByToken | |
100.00% |
1 / 1 |
2 | |
100.00% |
9 / 9 |
|||
updateUserPhoto | |
100.00% |
1 / 1 |
2 | |
100.00% |
6 / 6 |
|||
validatePhotoUploadForm | |
100.00% |
1 / 1 |
5 | |
100.00% |
14 / 14 |
|||
prepopulateUserProfile | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
validateBasicProfileForm | |
100.00% |
1 / 1 |
3 | |
100.00% |
12 / 12 |
|||
getTranslatedErrorMesssages | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
getAllLocales | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
<?php | |
/** | |
* Class UserManager | |
* | |
* @package AclUser\Service | |
* @author Nigel Hurnell | |
* @version v 1.0.0 | |
* @license BSD | |
* @copyright Copyright (c) 2017, Nigel Hurnell | |
*/ | |
namespace AclUser\Service; | |
use AclUser\Entity\User; | |
use AclUser\Entity\UserRoleMap; | |
use AclUser\Entity\Role; | |
use Zend\Authentication\Result; | |
use Zend\Crypt\Password\Bcrypt; | |
use Zend\Math\Rand; | |
use AclUser\Mail\MailMessage; | |
use Translate\Service\LanguageManager; | |
use Doctrine\ORM\EntityManager; | |
use AclUser\Service\RotateAndResizeImageFile; | |
/** | |
* This service is responsible for adding/editing users | |
* and changing user password. | |
* | |
* @package AclUser\Service | |
* @author Nigel Hurnell | |
* @version v 1.0.0 | |
* @license BSD | |
* @copyright Copyright (c) 2017, Nigel Hurnell | |
*/ | |
class UserManager | |
{ | |
const USER_IMAGE_FOLDER = './data/media/user-images/'; // Active user. | |
const PASSWORD_RESET_TOKEN_VALIDITY = 6 * 60 * 60; // 6 hours in seconds | |
/** | |
* Service that handles logic to actually sends an e-mail | |
* | |
* @var AclUser\Mail\MailMessage | |
*/ | |
protected $mailMessage; | |
/** | |
* Doctrine ORM manager/database abstraction | |
* | |
* @var EntityManager | |
*/ | |
protected $entityManager; | |
/** | |
* Doctrine ORM manager/database abstraction | |
* | |
* @var EntityManager | |
*/ | |
protected $languageManager; | |
/** | |
* Instantiate UserManager object and inject services | |
* | |
* @param Doctrine\ORM\EntityManager $entityManager | |
* @param AclUser\Mail\MailMessage $mailMessage | |
* @param Translate\Service\LanguageManager $languageManager | |
*/ | |
public function __construct(EntityManager $entityManager, MailMessage $mailMessage, LanguageManager $languageManager) | |
{ | |
$this->entityManager = $entityManager; | |
$this->mailMessage = $mailMessage; | |
$this->languageManager = $languageManager; | |
} | |
/** | |
* Grand specified user the basic role | |
* | |
* @param user $user | |
*/ | |
protected function giveUserBasicRole($user) | |
{ | |
$roleEntity = $this->entityManager->getRepository(Role::class) | |
->findOneBy(['name' => 'basic']); | |
$roleMap = new UserRoleMap(); | |
$roleMap->setUser($user); | |
$roleMap->setRole($roleEntity); | |
$this->entityManager->persist($roleMap); | |
// Apply changes to database. | |
$this->entityManager->flush(); | |
} | |
/** | |
* This method updates data of an existing user. | |
* | |
* @param type $user | |
* @param type $data | |
* @return boolean | |
* @throws \Exception | |
*/ | |
public function updateUser($user, $data) | |
{ | |
// Do not allow to change user email if another user with such email already exits. | |
if ($user->getEmail() != $data['email'] && $this->checkUserExists($data['email'])) { | |
throw new \Exception("Another user with email address " . $data['email'] . " already exists"); | |
} | |
$user->setEmail($data['email']); | |
$user->setFullName($data['full_name']); | |
$user->setStatus($data['status']); | |
// Apply changes to database. | |
$this->entityManager->flush(); | |
return true; | |
} | |
/** | |
* Checks whether an active user with given email address already exists in the database. | |
* | |
* @param string $email | |
* @return boolean whether the user exists in the database | |
*/ | |
public function checkUserExists($email) | |
{ | |
$user = $this->getUserByEmailAddress($email); | |
return $user !== null; | |
} | |
/** | |
* Find User by e-mail address | |
* | |
* @param string $email | |
* @return User | |
*/ | |
public function getUserByEmailAddress($email) | |
{ | |
return $this->entityManager->getRepository(User::class) | |
->findOneBy(['email' => $email]); | |
} | |
/** | |
* Find User by database id | |
* | |
* @param integer $id | |
* @return User AclUser/Entity/User entity | |
*/ | |
public function getUserById($id) | |
{ | |
return $this->entityManager->getRepository(User::class) | |
->findOneBy(['id' => $id]); | |
} | |
/** | |
* Checks that the given password is correct. | |
* | |
* @param User $user | |
* @param string $password | |
* @return boolean | |
*/ | |
public function validatePassword($user, $password) | |
{ | |
$bcrypt = new Bcrypt(); | |
return $bcrypt->verify($password, $user->getPassword()); | |
} | |
/** | |
* Generates a password reset token and save to database for this user. This token is then stored in database and | |
* sent to the user's E-mail address. When the user clicks the link in E-mail message, he is | |
* directed to the Set Password page. | |
* | |
* @param User $user | |
*/ | |
public function generatePasswordResetToken(User $user) | |
{ | |
// Generate a token. | |
$token = Rand::getString(32, '0123456789abcdefghijklmnopqrstuvwxyz', true); | |
$user->setPwdResetToken($token); | |
$user->setPwdResetTokenDate(new \DateTime()); | |
} | |
/** | |
* This method is used to change the password for the given user. To change the password, | |
* one must know the old password. | |
* | |
* @param User $user | |
* @param array $data | |
* @return boolean | |
*/ | |
public function changePassword($user, $data) | |
{ | |
$oldPassword = $data['old_password']; | |
// Check that old password is correct | |
if (!$this->validatePassword($user, $oldPassword)) { | |
return false; | |
} | |
$newPassword = $data['new_password']; | |
// Check password length | |
if (strlen($newPassword) < 6 || strlen($newPassword) > 64) { | |
return false; | |
} | |
$this->updateValidUsersPassword($user, $newPassword, false); | |
return true; | |
} | |
/** | |
* Get all Role object associated with this user | |
* | |
* @return ArrayCollection of Role entities | |
*/ | |
public function fetchAllRoles() | |
{ | |
return $this->entityManager->getRepository(Role::class) | |
->findAll(); | |
} | |
/** | |
* Get Role entity by role name | |
* | |
* @param string $roleName | |
* @return Role | |
*/ | |
public function getRoleByName($roleName) | |
{ | |
$role = $this->entityManager->getRepository(Role::class)->findOneBy( | |
array('name' => $roleName)); | |
return $role; | |
} | |
/** | |
* Get ArrayCollection of UserRoleMap entity objects | |
* | |
* @param integer $userId | |
* @return ArrayCollection of UserRoleMap entities | |
*/ | |
public function getRolesByUserId($userId) | |
{ | |
return $this->entityManager->getRepository(UserRoleMap::class)->findBy( | |
array('user_id' => $userId)); | |
} | |
/** | |
* Validate forgotten password for an redirect as required. | |
* | |
* @param ZendForm $form | |
* @param array $params | |
* @return Result | |
*/ | |
public function validateForgottenPasswordForm(\AclUser\Form\ForgottenPasswordForm $form, $params): Result | |
{ | |
$form->setData($params); | |
$result = new Result( | |
Result::FAILURE_IDENTITY_NOT_FOUND, null, []); | |
if ($form->isValid()) { | |
$data = $form->getData(); | |
// Look for the user with such email. | |
$user = $this->entityManager->getRepository(User::class) | |
->findOneBy(['email' => $data['email']]); | |
if ($user != null) { | |
// Generate a new password for user and send an E-mail | |
// notification about that. | |
$this->generatePasswordResetToken($user); | |
$this->entityManager->flush(); | |
$this->sendForgottenPasswordEmail($user); | |
$result = new Result(Result::SUCCESS, $user, ['success' => 'An e-mail has been sent your e-mail address.', 'email-success' => $data['email']]); | |
} else { | |
$result = new Result( | |
Result::SUCCESS, null, ['error' => 'This e-mail address does not have an account.', 'email' => $data['email']]); | |
} | |
} | |
return $result; | |
} | |
/** | |
* Validate | |
* | |
* @param \AclUser\Form\RegistrationForm $form | |
* @param array $params post parameters | |
* @param boolean $withCaptcha whether user is being created by new user or admin | |
* @return Result | |
*/ | |
public function validateRegistrationForm(\AclUser\Form\RegistrationForm $form, $params, $withCaptcha): Result | |
{ | |
$form->setData($params); | |
$result = new Result(Result::FAILURE_IDENTITY_NOT_FOUND, null, []); | |
if ($form->isValid()) { | |
$data = $form->getData(); | |
// Look for the user with such email. | |
if ($this->checkUserExists($data['email'])) { | |
$form->get('email')->setMessages(['There is already an account with this e-mail address.']); | |
} else { | |
$result = $this->createNewUser($data, $withCaptcha); | |
} | |
} | |
return $result; | |
} | |
/** | |
* Create System user and set messages and send e-mail depending if admin created user | |
* | |
* @param array $data post parameters | |
* @param boolean $withCaptcha indicates whether admin or anon is creating user | |
* @return Result | |
*/ | |
protected function createNewUser($data, $withCaptcha) | |
{ | |
$user = new User(); | |
$user->setPhoto(false); | |
$user->setEmail($data['email']); | |
$user->setFullName($data['full_name']); | |
if ($withCaptcha) { | |
$this->generatePasswordResetToken($user); | |
} | |
$user->setStatus($withCaptcha ? User::STATUS_RETIRED : User::STATUS_ACTIVE); | |
$user->setDateCreated(new \DateTime()); | |
$this->updateValidUsersPassword($user, $data['password'], true); | |
$this->giveUserBasicRole($user); | |
if ($withCaptcha) { | |
$this->sendConfirmNewAccountEmail($user); | |
$message = ['success' => 'A message has been sent to your e-mail address. Please follow the link to confirm your identity and activate your account.']; | |
} else { | |
$message = ['success' => 'Account Created Successfully!']; | |
} | |
return new Result(Result::SUCCESS, $user, $message); | |
} | |
/** | |
* Send e-mail when new user registers with application | |
* | |
* @param User $user the newly created user | |
* @param boolean $social whether account was created through social provider | |
*/ | |
public function sendConfirmNewAccountEmail(User $user, $social = false) | |
{ | |
$this->mailMessage | |
->setTo($user->getEmail(), $user->getFullName()) | |
->setSubject('Confirm Account Email') | |
->setViewScript('acl-user/email/confirm-account-email') | |
->setViewParams(['user' => $user, 'token' => $user->getPwdResetToken(), 'social' => $social]) | |
->embedImageFromSrc() | |
->setLayoutTemplate('layout/email-layout') | |
->sendEmailBasedOnViewScript(); | |
} | |
/** | |
* Send forgotten password e-mail with reset token | |
* | |
* @param User $user | |
*/ | |
protected function sendForgottenPasswordEmail(User $user) | |
{ | |
$this->mailMessage | |
->setTo($user->getEmail(), $user->getFullName()) | |
->setSubject('Password Reset') | |
->setViewScript('acl-user/email/forgotten-password-email') | |
->setLayoutTemplate('layout/email-layout') | |
->setViewParams(['user' => $user, 'token' => $user->getPwdResetToken()]) | |
->embedImageFromSrc() | |
->sendEmailBasedOnViewScript(); | |
} | |
/** | |
* Get the file path of the present user's image | |
* | |
* @param int $id | |
* @param boolean $permitted whether user is permitted to view the image | |
* | |
* @return string absolute file path to image of user | |
*/ | |
public function getUserPhotoLocationById($id, $permitted) | |
{ | |
$user = $this->getUserById($id); | |
$filepath = self::USER_IMAGE_FOLDER . 'avatar.png'; | |
if ($user && $user->getPhoto() && $permitted) { | |
$filepath = self::USER_IMAGE_FOLDER . $user->getId() . '.png'; | |
} | |
return $filepath; | |
} | |
/** | |
* Validate change password form | |
* | |
* @param \AclUser\Form\ChangePasswordForm $form | |
* @param array $params | |
* @param Acluser\Entity\user $user | |
* @return Result | |
*/ | |
public function validateChangePasswordForm(\AclUser\Form\ChangePasswordForm $form, $params, $user): Result | |
{ | |
$form->setData($params); | |
$result = new Result( | |
Result::FAILURE, null, ['error' => 'Form is not valid.']); | |
// Validate form | |
if ($form->isValid()) { | |
// Get filtered and validated data | |
$data = $form->getData(); | |
if (array_key_exists('old_password', $data) && !$this->validatePassword($user, $data['old_password'])) { | |
$form->get('old_password')->setMessages(['Old password is not correct']); | |
} else { | |
// note that form fails validation if form has old_password but params do not | |
$this->updateValidUsersPassword($user, $data['new_password']); | |
$result = new Result( | |
Result::SUCCESS, null, ['welcome' => 'Your password has been updated.']); | |
} | |
} | |
return $result; | |
} | |
/** | |
* Check that password reset token belongs to a user in the database and that | |
* it has not expired | |
* | |
* @param string $token the password reset token | |
* @return Result | |
*/ | |
public function checkResetToken($token): Result | |
{ | |
$user = $this->getUserByPasswordResetToken($token); | |
return $this->checkPasswordResetTokenForUser($user); | |
} | |
/** | |
* Validate reset password form and complete logic as appropriate | |
* | |
* @param \AclUser\Form\ResetPasswordForm $form | |
* @param array $params post parameters | |
* @return Result | |
*/ | |
public function validateResetPasswordForm(\AclUser\Form\ResetPasswordForm $form, $params): Result | |
{ | |
$form->setData($params); | |
$result = new Result( | |
Result::FAILURE_IDENTITY_NOT_FOUND, null, ['error' => 'Form is not valid.']); | |
if ($form->isValid()) { | |
$data = $form->getData(); | |
$user = $this->getUserByPasswordResetToken($data['token']); | |
$userResult = $this->checkPasswordResetTokenForUser($user); | |
if (!$userResult->isValid()) { | |
$result = $userResult; | |
} else if ($data['email'] !== $user->getEmail()) { | |
$form->get('email')->setMessages(['This email address does not correspond to the submitted password reset token']); | |
} else { | |
// Remove password reset token | |
$user->setPwdResetToken(null); | |
$user->setPwdResetTokenDate(null); | |
$this->updateValidUsersPassword($user, $data['new_password']); | |
$result = new Result(Result::SUCCESS, $user, ['success' => 'Your password has been updated and you have been successfully logged in.']); | |
} | |
} | |
return $result; | |
} | |
/** | |
* Check that user is not null and that their password reset token has not expired | |
* Add messages and result validity to Result depending on user status | |
* | |
* @todo update type declaration for $user to ?User (incompatible with phpdoc version) | |
* @param User|null $user | |
* @param string $message | |
* @return Result | |
*/ | |
protected function checkPasswordResetTokenForUser($user, $message = null): Result | |
{ | |
if ($user == null) { | |
return new Result(Result::FAILURE_CREDENTIAL_INVALID, null, ['error' => 'No user found for this reset token.']); | |
} | |
$tokenDate = $user->getPwdResetTokenDate(); | |
$currentDate = strtotime('now'); | |
if ($currentDate - $tokenDate->getTimestamp() > self::PASSWORD_RESET_TOKEN_VALIDITY) { | |
return new Result(Result::FAILURE_CREDENTIAL_INVALID, null, ['error' => 'The password reset token has expired.']); | |
} | |
return new Result(Result::SUCCESS, $user, [$message]); | |
} | |
/** | |
* Get user from database by password reset token | |
* | |
* @param string $token the password reset token | |
* @return User|null if user is not found in database | |
*/ | |
protected function getUserByPasswordResetToken($token) | |
{ | |
return $this->entityManager->getRepository(User::class) | |
->findOneBy(['pwdResetToken' => $token]); | |
} | |
/** | |
* Hash password and update database with hash for this user | |
* | |
* @param User $user entity object | |
* @param string $password un-hashed password | |
* @param boolean $newUser whether this a newly created entity | |
*/ | |
public function updateValidUsersPassword(User $user, $password, $newUser = false) | |
{ | |
$bcrypt = new Bcrypt(); | |
$passwordHash = $bcrypt->create($password); | |
$user->setPassword($passwordHash); | |
if ($newUser) { | |
// Add the entity to the entity manager. | |
$this->entityManager->persist($user); | |
} | |
// Apply changes to database. | |
$this->entityManager->flush(); | |
} | |
/** | |
* Check that token belongs to a user then change their status to active if | |
* it does and update pwdResetToken and date to null. Either was assign | |
* feedback messages to the returned Result object | |
* | |
* @param string $token | |
* @return Result | |
*/ | |
public function activateAccountByToken($token) | |
{ | |
$result = new Result(Result::FAILURE, null, ['error' => 'No user found for this account activation token.']); | |
$user = $this->getUserByPasswordResetToken($token); | |
if ($user != null) { | |
$user->setPwdResetToken(null); | |
$user->setPwdResetTokenDate(null); | |
$user->setStatus(User::STATUS_ACTIVE); | |
$this->entityManager->flush(); | |
$result = new Result(Result::SUCCESS, null, ['success' => 'Your account has been activated. Log in with your e-mail and password or Social Media provider.']); | |
} | |
return $result; | |
} | |
/** | |
* Get entity object for the user and update whether they have uploaded their photo | |
* | |
* @param integer $id the id of the user | |
* @param boolean $status the status of the user | |
*/ | |
public function updateUserPhoto($id, $status) | |
{ | |
$user = $this->entityManager->getRepository(User::class) | |
->findOneBy(['id' => $id]); | |
if ($user) { | |
$user->setPhoto($status); | |
$this->entityManager->flush(); | |
} | |
} | |
/** | |
* Validate user photo upload and perform transformation on same | |
* | |
* @param boolean $isPost | |
* @param AclUser\Form\RotateAndResizeImageForm $form | |
* @param \Zend\Mvc\Controller\Plugin\Params $params | |
* @param integer $userId | |
* @return array | |
*/ | |
public function validatePhotoUploadForm($isPost, $form, \Zend\Mvc\Controller\Plugin\Params $params, $userId) | |
{ | |
$result = ['success' => false, 'errors' => ['Form Was Not Submitted by Post']]; | |
if ($isPost) { | |
$data = array_merge_recursive( | |
$params->fromPost(), $params->fromFiles() | |
); | |
$form->setData($data); | |
if ($form->isValid()) { | |
$data = $form->getData(); | |
unset($data['file']); | |
$rotator = new RotateAndResizeImageFile(); | |
$result['success'] = $rotator->rotateAndResize($data, $userId); | |
$result['success'] ? $this->updateUserPhoto($userId, true) : null; | |
$result['errors'] = $result['success'] ? [] : $rotator->getErrorMessages(); | |
} else { | |
$result['errors'] = $form->getMessages(); | |
} | |
} | |
return $result; | |
} | |
/** | |
* Pre-populate BasicProfileForm with the existing values for user corresponding to the user id | |
* | |
* @param \AclUser\Form\BasicProfileForm $form | |
* @param integer $id | |
*/ | |
public function prepopulateUserProfile($form, $id) | |
{ | |
$user = $this->entityManager->getRepository(User::class) | |
->findOneBy(['id' => $id]); | |
if ($user) { | |
$form->get('full_name')->setAttribute('placeholder', $user->getFullName()); | |
$form->get('email')->setAttribute('placeholder', $user->getEmail()); | |
} | |
return isset($user) ? true : false; | |
} | |
/** | |
* Validate posted parameters from BasicProfileForm and update user if they are valid | |
* | |
* @param \AclUser\Form\BasicProfileForm $form | |
* @param array $params the post parameters | |
* @param integer $id the logged in user id or the id of the user being updated | |
* @return boolean | |
*/ | |
public function validateBasicProfileForm($form, $params, $id) | |
{ | |
$success = false; | |
$form->setData($params); | |
if ($form->isValid()) { | |
$data = $form->getData(); | |
$user = $this->entityManager->getRepository(User::class) | |
->findOneBy(['id' => $id]); | |
if ($user) { | |
$success = true; | |
$user->setFullName($data['full_name']); | |
$user->setEmail($data['email']); | |
$this->entityManager->flush(); | |
} | |
} | |
return $success; | |
} | |
/** | |
* Translate all error messages | |
* | |
* @param Translate\Mvc\Controller\Plugin\TranslateControllerPlugin $translateContollerPlugin translator controller plugin | |
* @param array $errorMessages untranslated error messages | |
* @return array of translated error messages | |
*/ | |
public function getTranslatedErrorMesssages($translateContollerPlugin, $errorMessages) | |
{ | |
$errors = []; | |
foreach ($errorMessages as $error) { | |
$errors[] = $translateContollerPlugin->translate($error); | |
} | |
return $errors; | |
} | |
/** | |
* Get an array of all locales that (could be) available to application | |
* | |
* @return array | |
*/ | |
public function getAllLocales() | |
{ | |
return $this->languageManager->getAllLocales(); | |
} | |
} |