2016-08-26 12:43:45 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Vn\Web;
|
|
|
|
|
2017-05-02 12:33:48 +00:00
|
|
|
use Vn\Db;
|
2016-08-26 12:43:45 +00:00
|
|
|
use Vn\Lib\Locale;
|
2016-09-23 22:47:34 +00:00
|
|
|
use Vn\Lib\UserException;
|
2016-08-26 12:43:45 +00:00
|
|
|
|
2016-10-04 15:27:49 +00:00
|
|
|
const MIN = 60;
|
|
|
|
const HOUR = 60 * MIN;
|
|
|
|
const DAY = 24 * HOUR;
|
|
|
|
const WEEK = 7 * DAY;
|
|
|
|
|
2016-08-26 12:43:45 +00:00
|
|
|
/**
|
|
|
|
* Thrown when user credentials could not be fetched.
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2016-09-23 22:47:34 +00:00
|
|
|
class SessionExpiredException extends UserException {}
|
2016-08-26 12:43:45 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Thrown when user credentials are invalid.
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2016-09-23 22:47:34 +00:00
|
|
|
class BadLoginException extends UserException {}
|
2016-08-26 12:43:45 +00:00
|
|
|
|
2019-10-29 13:02:09 +00:00
|
|
|
/**
|
|
|
|
* Thrown when user credentials are invalid.
|
|
|
|
*/
|
|
|
|
class UserDisabledException extends UserException {}
|
|
|
|
|
2016-08-26 12:43:45 +00:00
|
|
|
/**
|
|
|
|
* Thrown when client version is outdated.
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2016-09-23 22:47:34 +00:00
|
|
|
class OutdatedVersionException extends UserException {}
|
2016-08-26 12:43:45 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Main class for web applications.
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
abstract class Service {
|
2016-08-26 12:43:45 +00:00
|
|
|
protected $app;
|
2016-09-23 22:47:34 +00:00
|
|
|
protected $db;
|
|
|
|
protected $userDb = NULL;
|
2016-08-26 12:43:45 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
function __construct($app) {
|
2016-08-26 12:43:45 +00:00
|
|
|
$this->app = $app;
|
2017-11-29 10:01:48 +00:00
|
|
|
}
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
function init() {
|
|
|
|
$this->db = $this->app->getSysConn();
|
2016-08-26 12:43:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts the user session.
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function startSession() {
|
|
|
|
$db = $this->app->getSysConn();
|
2016-09-19 06:40:18 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
ini_set('session.cookie_secure', $this->isHttps());
|
|
|
|
ini_set('session.hash_function', 'sha256');
|
2016-09-19 06:40:18 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
session_set_save_handler(new DbSessionHandler($db));
|
|
|
|
session_start();
|
2016-09-19 06:40:18 +00:00
|
|
|
|
2016-08-26 12:43:45 +00:00
|
|
|
// Setting the locale
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
|
|
|
|
if (!isset($_SESSION['httpLanguage'])
|
|
|
|
|| $_SESSION['httpLanguage'] != $_SERVER['HTTP_ACCEPT_LANGUAGE']) {
|
2016-09-19 06:40:18 +00:00
|
|
|
$_SESSION['httpLanguage'] = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
|
2016-08-26 12:43:45 +00:00
|
|
|
$regexp = '/([a-z]{1,4})(?:-[a-z]{1,4})?\s*(?:;\s*q\s*=\s*(?:1|0\.[0-9]+))?,?/i';
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
preg_match_all($regexp, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $languages);
|
2016-08-26 12:43:45 +00:00
|
|
|
|
2018-06-06 11:08:17 +00:00
|
|
|
foreach ($languages[1] as $lang)
|
2018-05-23 10:14:20 +00:00
|
|
|
if (TRUE || stream_resolve_include_path("locale/$lang")) {
|
2016-08-26 12:43:45 +00:00
|
|
|
$_SESSION['lang'] = $lang;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (!isset($_SESSION['lang']))
|
2016-08-26 12:43:45 +00:00
|
|
|
$_SESSION['lang'] = NULL;
|
2016-11-08 07:00:02 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
Locale::set($_SESSION['lang']);
|
|
|
|
Locale::addPath('vn/web');
|
2016-08-26 12:43:45 +00:00
|
|
|
|
|
|
|
// Registering the visit
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($_COOKIE['PHPSESSID'])
|
|
|
|
|| isset($_SESSION['access'])
|
|
|
|
|| isset($_SESSION['skipVisit'])
|
|
|
|
|| !isset($_SERVER['HTTP_USER_AGENT']))
|
2016-08-26 12:43:45 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
$agent = $_SERVER['HTTP_USER_AGENT'];
|
2018-05-23 10:14:20 +00:00
|
|
|
$browser = get_browser($agent, TRUE);
|
2016-08-26 12:43:45 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (!empty($browser['crawler'])) {
|
2016-08-26 12:43:45 +00:00
|
|
|
$_SESSION['skipVisit'] = TRUE;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($_SERVER['REMOTE_ADDR']))
|
|
|
|
$ip = ip2long($_SERVER['REMOTE_ADDR']);
|
2016-08-26 12:43:45 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
$row = $db->getRow(
|
2019-05-21 14:16:27 +00:00
|
|
|
'CALL visit_register(#, #, #, #, #, #, #, #, #)',
|
2016-08-26 12:43:45 +00:00
|
|
|
[
|
2018-05-23 10:14:20 +00:00
|
|
|
nullIf($_COOKIE, 'vnVisit')
|
|
|
|
,nullIf($browser, 'platform')
|
|
|
|
,nullIf($browser, 'browser')
|
|
|
|
,nullIf($browser, 'version')
|
|
|
|
,nullIf($browser, 'javascript')
|
|
|
|
,nullIf($browser, 'cookies')
|
|
|
|
,isset($agent) ? $agent : NULL
|
|
|
|
,isset($ip) && $ip ? $ip : NULL
|
|
|
|
,nullIf($_SERVER, 'HTTP_REFERER')
|
2016-08-26 12:43:45 +00:00
|
|
|
]
|
|
|
|
);
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($row['access'])) {
|
|
|
|
setcookie('vnVisit', $row['visit'], time() + 31536000); // 1 Year
|
2016-08-26 12:43:45 +00:00
|
|
|
$_SESSION['access'] = $row['access'];
|
2018-05-23 11:09:55 +00:00
|
|
|
} else
|
2016-08-26 12:43:45 +00:00
|
|
|
$_SESSION['skipVisit'] = TRUE;
|
|
|
|
}
|
2016-09-20 18:36:22 +00:00
|
|
|
|
|
|
|
/**
|
2019-07-02 08:48:14 +00:00
|
|
|
* Authenticates the user with it's credentials or token.
|
2016-09-20 18:36:22 +00:00
|
|
|
*
|
|
|
|
* return Db\Conn The database connection
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function login() {
|
2016-09-23 22:47:34 +00:00
|
|
|
$db = $this->db;
|
2016-10-15 18:58:30 +00:00
|
|
|
$anonymousUser = FALSE;
|
2016-09-20 18:36:22 +00:00
|
|
|
|
2019-07-02 08:48:14 +00:00
|
|
|
if (isset($_POST['user']) && !empty($_POST['password'])) {
|
2018-05-23 10:14:20 +00:00
|
|
|
$user = strtolower($_POST['user']);
|
2019-07-02 08:48:14 +00:00
|
|
|
|
|
|
|
$passwordHash = $db->getValue(
|
2019-10-29 13:02:09 +00:00
|
|
|
'SELECT bcryptPassword FROM account.user WHERE `name` = #',
|
2019-07-02 08:48:14 +00:00
|
|
|
[$user]
|
|
|
|
);
|
|
|
|
|
|
|
|
$passwordOk = !empty($passwordHash)
|
|
|
|
&& password_verify($_POST['password'], $passwordHash);
|
|
|
|
|
|
|
|
// XXX: Compatibility with old MD5 passwords
|
|
|
|
if (empty($passwordHash)) {
|
|
|
|
$md5Password = $db->getValue(
|
|
|
|
'SELECT `password` FROM account.user
|
2019-07-02 09:42:39 +00:00
|
|
|
WHERE active AND `name` = #',
|
2019-07-02 08:48:14 +00:00
|
|
|
[$user]
|
|
|
|
);
|
|
|
|
|
|
|
|
$passwordOk = !empty($md5Password)
|
|
|
|
&& $md5Password == md5($_POST['password']);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$passwordOk) {
|
2019-07-02 08:52:32 +00:00
|
|
|
sleep(3);
|
2019-07-02 08:48:14 +00:00
|
|
|
throw new BadLoginException();
|
2016-09-20 18:36:22 +00:00
|
|
|
}
|
2018-05-23 11:09:55 +00:00
|
|
|
} else {
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($_POST['token']) || isset($_GET['token'])) {
|
|
|
|
if (isset($_POST['token']))
|
2016-09-20 18:36:22 +00:00
|
|
|
$token = $_POST['token'];
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($_GET['token']))
|
2016-09-20 18:36:22 +00:00
|
|
|
$token = $_GET['token'];
|
2016-09-24 14:32:31 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
$key = $db->getValue('SELECT jwtKey FROM config');
|
2016-11-14 09:47:39 +00:00
|
|
|
|
|
|
|
try {
|
2018-05-23 10:14:20 +00:00
|
|
|
$jwtPayload = Jwt::decode($token, $key);
|
2018-05-23 11:09:55 +00:00
|
|
|
} catch (\Exception $e) {
|
2018-05-23 10:14:20 +00:00
|
|
|
throw new BadLoginException($e->getMessage());
|
2016-11-14 09:47:39 +00:00
|
|
|
}
|
|
|
|
|
2016-09-20 18:36:22 +00:00
|
|
|
$expiration = $jwtPayload['exp'];
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (empty($expiration) || $expiration <= time())
|
|
|
|
throw new SessionExpiredException();
|
2016-09-20 18:36:22 +00:00
|
|
|
|
2016-09-24 14:32:31 +00:00
|
|
|
$user = $jwtPayload['sub'];
|
2016-10-14 10:58:35 +00:00
|
|
|
}
|
2018-05-23 10:14:20 +00:00
|
|
|
else {
|
|
|
|
$user = $db->getValue('SELECT guestUser FROM config');
|
2016-10-15 18:58:30 +00:00
|
|
|
$anonymousUser = TRUE;
|
|
|
|
}
|
2016-09-20 18:36:22 +00:00
|
|
|
}
|
|
|
|
|
2019-10-29 13:02:09 +00:00
|
|
|
if (!$anonymousUser) {
|
|
|
|
$isActive = $db->getValue(
|
|
|
|
'SELECT active FROM account.user WHERE `name` = #',
|
|
|
|
[$user]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!$isActive)
|
|
|
|
throw new UserDisabledException();
|
|
|
|
}
|
|
|
|
|
2020-10-23 10:10:41 +00:00
|
|
|
$db->query('CALL account.myUser_loginWithName(#)', [$user]);
|
2019-07-02 11:43:52 +00:00
|
|
|
|
2016-10-15 18:58:30 +00:00
|
|
|
$userChanged = !$anonymousUser
|
2018-05-23 10:14:20 +00:00
|
|
|
&&(empty($_SESSION['user']) || $_SESSION['user'] != $user);
|
2016-10-15 18:58:30 +00:00
|
|
|
|
2016-09-20 18:36:22 +00:00
|
|
|
$_SESSION['user'] = $user;
|
|
|
|
|
|
|
|
// Registering the user access
|
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($_SESSION['access']) && $userChanged)
|
|
|
|
$db->query(
|
2019-05-21 14:16:27 +00:00
|
|
|
'CALL visitUser_new(#, #)',
|
2018-05-23 10:14:20 +00:00
|
|
|
[$_SESSION['access'], session_id()]
|
2016-09-20 18:36:22 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logouts the current user. Cleans the last saved used credentials.
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function logout() {
|
|
|
|
unset($_SESSION['user']);
|
2016-09-20 18:36:22 +00:00
|
|
|
}
|
2016-09-23 22:47:34 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates or returns a database connection where the authenticated user
|
2018-03-26 16:35:02 +00:00
|
|
|
* is the role of the current logged user.
|
2016-09-23 22:47:34 +00:00
|
|
|
*
|
|
|
|
* @return {Db\Conn} The database connection
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function getUserDb($user) {
|
2016-09-23 22:47:34 +00:00
|
|
|
if ($this->userDb)
|
|
|
|
return $this->userDb;
|
2018-03-26 16:35:02 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
$row = $this->db->getObject(
|
2022-02-10 11:37:12 +00:00
|
|
|
'SELECT r.name, rc.mysqlPassword, rc.rolePrefix, uc.loginKey
|
2018-03-26 16:35:02 +00:00
|
|
|
FROM account.user u
|
|
|
|
JOIN account.role r ON r.id = u.role
|
|
|
|
JOIN account.roleConfig rc ON TRUE
|
|
|
|
JOIN account.userConfig uc ON TRUE
|
|
|
|
WHERE u.name = #',
|
|
|
|
[$user]
|
|
|
|
);
|
|
|
|
|
2022-02-10 11:37:12 +00:00
|
|
|
$userName = "{$row->rolePrefix}{$row->name}";
|
2018-05-23 10:14:20 +00:00
|
|
|
$password = base64_decode($row->mysqlPassword);
|
|
|
|
$userDb = $this->app->createConnection($userName, $password, TRUE);
|
2018-03-26 16:35:02 +00:00
|
|
|
|
2020-10-23 10:10:41 +00:00
|
|
|
$userDb->query('CALL account.myUser_loginWithKey(#, #)', [$user, $row->loginKey]);
|
2018-03-26 16:35:02 +00:00
|
|
|
return $userDb;
|
2016-09-23 22:47:34 +00:00
|
|
|
}
|
2016-10-04 15:27:49 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a JWT authentication token for the specified $user.
|
|
|
|
*
|
|
|
|
* @param {string} $user The user name
|
|
|
|
* @param {boolean} $remember Wether to create long live token
|
2016-10-14 10:58:35 +00:00
|
|
|
* @param {boolean} $recover Wether to enable recovery mode on login
|
2016-10-04 15:27:49 +00:00
|
|
|
* @return {string} The JWT generated token
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2021-03-31 10:18:25 +00:00
|
|
|
function createToken($user, $remember = FALSE) {
|
2016-10-04 15:27:49 +00:00
|
|
|
if ($remember)
|
|
|
|
$tokenLife = WEEK;
|
|
|
|
else
|
|
|
|
$tokenLife = 30 * MIN;
|
|
|
|
|
|
|
|
$payload = [
|
|
|
|
'sub' => $user,
|
2018-05-23 10:14:20 +00:00
|
|
|
'exp' => time() + $tokenLife
|
2016-10-04 15:27:49 +00:00
|
|
|
];
|
2016-10-13 15:07:48 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
$key = $this->db->getValue('SELECT jwtKey FROM config');
|
|
|
|
return Jwt::encode($payload, $key);
|
2016-10-04 15:27:49 +00:00
|
|
|
}
|
2017-11-29 10:01:48 +00:00
|
|
|
|
2016-09-23 22:47:34 +00:00
|
|
|
/**
|
2017-11-29 10:01:48 +00:00
|
|
|
* Obtains the application version number. It is extracted and
|
|
|
|
* cached from package.json file.
|
|
|
|
*
|
|
|
|
* @return string The version number
|
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function getVersion() {
|
|
|
|
$appName = $this->app->getName();
|
2017-11-29 10:01:48 +00:00
|
|
|
$version = apc_fetch("$appName.version", $success);
|
2016-09-23 22:47:34 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (!$success) {
|
|
|
|
if (file_exists('package.json')) {
|
|
|
|
$package = json_decode(file_get_contents('package.json'));
|
2017-11-29 10:01:48 +00:00
|
|
|
$version = $package->version;
|
2018-05-23 11:09:55 +00:00
|
|
|
} else
|
2017-11-29 10:01:48 +00:00
|
|
|
$version = '0.0.0';
|
2016-09-23 22:47:34 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
apc_store("$appName.version", $version);
|
2017-11-29 10:01:48 +00:00
|
|
|
}
|
2017-05-02 12:33:48 +00:00
|
|
|
|
2017-11-29 10:01:48 +00:00
|
|
|
return $version;
|
|
|
|
}
|
2017-05-22 07:49:05 +00:00
|
|
|
|
2017-11-29 10:01:48 +00:00
|
|
|
/**
|
|
|
|
* Checks the client version.
|
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function checkVersion() {
|
|
|
|
if (!empty($_COOKIE['vnVersion']))
|
2017-11-29 10:01:48 +00:00
|
|
|
$clientVersion = $_COOKIE['vnVersion'];
|
2016-09-23 22:47:34 +00:00
|
|
|
|
2018-05-23 10:14:20 +00:00
|
|
|
if (isset($clientVersion)
|
|
|
|
&& $clientVersion < $this->getVersion())
|
|
|
|
throw new OutdatedVersionException();
|
2016-09-23 22:47:34 +00:00
|
|
|
}
|
2016-08-26 12:43:45 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the HTTP connection is secure.
|
|
|
|
*
|
|
|
|
* @return boolean Return %TRUE if its secure, %FALSE otherwise
|
2017-11-29 10:01:48 +00:00
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function isHttps() {
|
|
|
|
return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
|
2016-08-26 12:43:45 +00:00
|
|
|
}
|
2017-11-29 10:01:48 +00:00
|
|
|
|
2016-08-26 12:43:45 +00:00
|
|
|
/**
|
2017-11-29 10:01:48 +00:00
|
|
|
* Returns the current URI without the GET part.
|
2016-09-24 14:32:31 +00:00
|
|
|
*
|
2017-11-29 10:01:48 +00:00
|
|
|
* @return string The current URI
|
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function getUri() {
|
2017-11-29 10:01:48 +00:00
|
|
|
return "{$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}";
|
2016-08-26 12:43:45 +00:00
|
|
|
}
|
2016-09-24 14:32:31 +00:00
|
|
|
|
|
|
|
/**
|
2017-11-29 10:01:48 +00:00
|
|
|
* Returns the current URL without the GET part.
|
2016-09-24 14:32:31 +00:00
|
|
|
*
|
2017-11-29 10:01:48 +00:00
|
|
|
* @return string The current URL
|
|
|
|
*/
|
2018-05-23 10:14:20 +00:00
|
|
|
function getUrl() {
|
|
|
|
$proto = $this->isHttps() ? 'https' : 'http';
|
2017-11-29 10:01:48 +00:00
|
|
|
return "$proto://{$this->getUri()}";
|
2016-09-24 14:32:31 +00:00
|
|
|
}
|
2016-08-26 12:43:45 +00:00
|
|
|
}
|