src/User/UserManager.php
<?php
/**
* This file is part of openvj project.
*
* Copyright 2013-2015 openvj dev team.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace VJ\User;
use Respect\Validation\Validator;
use Symfony\Component\HttpFoundation\Cookie;
use VJ\Core\Application;
use VJ\Core\Exception\InvalidArgumentException;
use VJ\Core\Exception\UserException;
use VJ\Core\Request;
use VJ\Core\Response;
use VJ\Security\KeywordFilter;
use VJ\VJ;
class UserManager
{
private $session;
private $request;
private $response;
public $user_credential;
/**
* @param UserSession $session
* @param Request $request
* @param Response $response
* @param UserCredential $user_credential
*/
public function __construct(
UserSession $session,
Request $request,
Response $response,
UserCredential $user_credential
) {
$this->session = $session;
$this->request = $request;
$this->response = $response;
$this->user_credential = $user_credential;
}
/**
* 成功登录后初始化 session
*
* @param array $user
* @param int $from
*/
private function prepareLoginSessionByObject(array $user, $from)
{
$this->session->start();
$this->session->invalidate();
$this->session->generateCsrfToken();
$this->session->set('user', $user);
$this->session->set('loginType', $from);
}
/**
* 用户名密码登录
*
* @param string $usernameEmail
* @param string $password
* @param bool $remember
* @return array
* @throws UserException
*/
public function interactiveLogin($usernameEmail, $password, $remember = false)
{
if (!is_string($usernameEmail)) {
throw new InvalidArgumentException('usernameEmail', 'type_invalid');
}
if (!is_string($password)) {
throw new InvalidArgumentException('password', 'type_invalid');
}
$user = $this->user_credential->checkPasswordCredential($usernameEmail, $password);
if ($remember) {
$this->generateRememberMeTokenForObject($user);
}
$this->prepareLoginSessionByObject($user, VJ::LOGIN_TYPE_INTERACTIVE);
return $user;
}
/**
* 用户是否提供了 REMEMBERME_TOKEN
*
* @return bool
*/
public function isRememberMeTokenProvided()
{
$token_field = Application::get('config')['session']['remember_token'];
$clientToken = $this->request->cookies->get($token_field);
return ($clientToken !== null);
}
/**
* Cookie 已记忆会话登录
*
* @return array
* @throws UserException
*/
public function rememberMeTokenLogin()
{
$token_field = Application::get('config')['session']['remember_token'];
$clientToken = $this->request->cookies->get($token_field);
try {
$user = $this->user_credential->checkRememberMeTokenCredential($clientToken);
} catch (UserException $e) {
// 对于无效 token 需要删除 cookie
$this->invalidateRememberMeToken();
throw $e;
}
// 对于有效 token,需要重新生成一份新 token,并继承其过期时间
$token = $this->user_credential->rememberme_encoder->parseClientToken($clientToken);
$this->invalidateRememberMeToken();
$this->generateRememberMeTokenForObject($user, $token['expire']);
$this->prepareLoginSessionByObject($user, VJ::LOGIN_TYPE_TOKEN);
return $user;
}
/**
* 无效化已记忆会话
*/
public function invalidateRememberMeToken()
{
$token_field = Application::get('config')['session']['remember_token'];
$clientToken = $this->request->cookies->get($token_field);
if ($clientToken !== null) {
$this->user_credential->invalidateRememberMeToken($clientToken);
}
$this->request->cookies->remove($token_field);
$this->response->headers->clearCookie($token_field);
}
/**
* 创建一个记忆会话
*
* @param array $user
* @param int|null $expire
*/
public function generateRememberMeTokenForObject(array $user, $expire = null)
{
$token_field = Application::get('config')['session']['remember_token'];
if ($expire === null) {
$expire = time() + (int)Application::get('config')['session']['remember_ttl'];
}
$clientToken = $this->user_credential->createRememberMeToken($user['uid'],
$this->request->getClientIp(),
$this->request->getUserAgent(),
$expire
);
$this->request->cookies->set($token_field, $clientToken);
$this->response->headers->setCookie(new Cookie($token_field, $clientToken, $expire));
}
/**
* 用户是否已登录
*
* @return bool
*/
public function isLoggedIn()
{
return $this->session->get('user') !== null;
}
/**
* 登出用户当前会话
*/
public function logout()
{
// 如果有已记忆会话,需要清除
$this->invalidateRememberMeToken();
$this->session->invalidate();
}
/**
* 创建用户
*
* @param string $username
* @param string $password
* @param string $email
* @return int UID
* @throws InvalidArgumentException
* @throws UserException
*/
public function createUser($username, $password, $email)
{
if (!is_string($username)) {
throw new InvalidArgumentException('username', 'type_invalid');
}
if (!is_string($password)) {
throw new InvalidArgumentException('password', 'type_invalid');
}
if (!is_string($email)) {
throw new InvalidArgumentException('email', 'type_invalid');
}
// 检查用户名
if (!mb_check_encoding($username, 'UTF-8')) {
throw new InvalidArgumentException('username', 'encoding_invalid');
}
$username = trim($username);
if (!Validator::regex('/^\S*$/')->length(3, 16)->validate($username)) {
throw new InvalidArgumentException('username', 'format_invalid');
}
// 检查关键字
$keyword = KeywordFilter::isContainGeneric($username);
if ($keyword === false) {
$keyword = KeywordFilter::isContainName($username);
}
if ($keyword !== false) {
throw new UserException('UserManager.name_forbid', [
'keyword' => $keyword
]);
}
// 检查密码
if (!Validator::length(0, 50)->validate($password)) {
throw new InvalidArgumentException('password', 'format_invalid');
}
// 检查 email
if (!Validator::email()->validate($email)) {
throw new InvalidArgumentException('password', 'format_invalid');
}
// 处理用户名
$username = VJ::removeEmoji($username);
// 检查用户名和 Email 是否唯一
if (UserUtil::getUserObjectByUsername($username) !== null) {
throw new UserException('UserManager.createUser.user_exists');
}
if (UserUtil::getUserObjectByEmail($email) !== null) {
throw new UserException('UserManager.createUser.email_exists');
}
// 生成 hash & salt
$hashSaltPair = $this->user_credential->password_encoder->generateHash($password);
// 插入记录
try {
$_id = new \MongoId();
$doc = [
'_id' => $_id,
'uid' => $_id, // 将在成功插入后更新
'user' => $username,
'luser' => UserUtil::canonicalizeUsername($username),
'mail' => $email,
'lmail' => UserUtil::canonicalizeEmail($email),
'salt' => $hashSaltPair['salt'],
'hash' => $hashSaltPair['hash'],
'g' => $email,
'gender' => VJ::USER_GENDER_UNKNOWN,
'regat' => new \MongoDate(),
'regip' => $this->request->getClientIp(),
];
Application::coll('User')->insert($doc);
} catch (\MongoCursorException $e) {
// 插入失败
throw new UserException('UserManager.createUser.user_or_email_exists');
}
// 插入成功:更新 uid
// 获取递增 uid
$counterRec = Application::coll('System')->findAndModify([
'_id' => 'UserCounter'
], [
'$inc' => ['count' => 1]
], [], [
'new' => true,
'upsert' => true
]);
$uid = (int)$counterRec['count'];
try {
// 修改 uid
Application::coll('User')->update([
'_id' => $_id
], [
'$set' => ['uid' => $uid]
]);
} catch (\MongoCursorException $e) {
// 修改 uid 失败(uid 重复),则删除用户记录
Application::critical('createUser.uidDuplicate', ['uid' => $uid]);
Application::coll('User')->remove(['_id' => $_id], ['justOne' => true]);
throw new UserException('UserManager.createUser.internal');
}
Application::emit('user.created', [$uid]);
return $uid;
}
}