Determine client info on insert rather than on retrieve for speed improvements.
i suppose device detect only ever expects to analyse a single string at once given its made for matomo so it on the slower side for multiple dingusses
This commit is contained in:
parent
1094f8dc67
commit
52aa43dd94
6 changed files with 156 additions and 47 deletions
51
database/2023_07_21_121854_update_user_agent_storage.php
Normal file
51
database/2023_07_21_121854_update_user_agent_storage.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
use Index\Data\IDbConnection;
|
||||||
|
use Index\Data\Migration\IDbMigration;
|
||||||
|
use Misuzu\ClientInfo;
|
||||||
|
|
||||||
|
final class UpdateUserAgentStorage_20230721_121854 implements IDbMigration {
|
||||||
|
public function migrate(IDbConnection $conn): void {
|
||||||
|
// convert user agent fields to BLOB and add field for client info storage
|
||||||
|
$conn->execute('
|
||||||
|
ALTER TABLE msz_login_attempts
|
||||||
|
CHANGE COLUMN attempt_user_agent attempt_user_agent TEXT NOT NULL COLLATE "utf8mb4_bin" AFTER attempt_created,
|
||||||
|
ADD COLUMN attempt_client_info TEXT NULL DEFAULT NULL AFTER attempt_user_agent
|
||||||
|
');
|
||||||
|
$conn->execute('
|
||||||
|
ALTER TABLE msz_sessions
|
||||||
|
CHANGE column session_user_agent session_user_agent TEXT NOT NULL COLLATE "utf8mb4_bin" AFTER session_ip_last,
|
||||||
|
ADD COLUMN session_client_info TEXT NULL DEFAULT NULL AFTER session_user_agent
|
||||||
|
');
|
||||||
|
|
||||||
|
// make sure all existing fields have client info fields filled
|
||||||
|
$updateLoginAttempts = $conn->prepare('UPDATE msz_login_attempts SET attempt_client_info = ? WHERE attempt_user_agent = ?');
|
||||||
|
$selectLoginAttempts = $conn->query('SELECT DISTINCT attempt_user_agent FROM msz_login_attempts');
|
||||||
|
while($selectLoginAttempts->next()) {
|
||||||
|
$updateLoginAttempts->reset();
|
||||||
|
$userAgent = $selectLoginAttempts->getString(0);
|
||||||
|
$updateLoginAttempts->addParameter(1, ClientInfo::parse($userAgent)->encode());
|
||||||
|
$updateLoginAttempts->addParameter(2, $userAgent);
|
||||||
|
$updateLoginAttempts->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
$updateSessions = $conn->prepare('UPDATE msz_sessions SET session_client_info = ? WHERE session_user_agent = ?');
|
||||||
|
$selectSessions = $conn->query('SELECT DISTINCT session_user_agent FROM msz_sessions');
|
||||||
|
while($selectSessions->next()) {
|
||||||
|
$updateSessions->reset();
|
||||||
|
$userAgent = $selectSessions->getString(0);
|
||||||
|
$updateSessions->addParameter(1, ClientInfo::parse($userAgent)->encode());
|
||||||
|
$updateSessions->addParameter(2, $userAgent);
|
||||||
|
$updateSessions->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make client info fields NOT NULL
|
||||||
|
$conn->execute('
|
||||||
|
ALTER TABLE msz_login_attempts
|
||||||
|
CHANGE COLUMN attempt_client_info attempt_client_info TEXT NOT NULL COLLATE "utf8mb4_bin" AFTER attempt_user_agent
|
||||||
|
');
|
||||||
|
$conn->execute('
|
||||||
|
ALTER TABLE msz_sessions
|
||||||
|
CHANGE COLUMN session_client_info session_client_info TEXT NOT NULL COLLATE "utf8mb4_bin" AFTER session_user_agent
|
||||||
|
');
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,9 @@
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
|
use Misuzu\Users\UserNotFoundException;
|
||||||
use Misuzu\Users\UserSession;
|
use Misuzu\Users\UserSession;
|
||||||
|
use Misuzu\Users\UserSessionNotFoundException;
|
||||||
|
|
||||||
require_once __DIR__ . '/../misuzu.php';
|
require_once __DIR__ . '/../misuzu.php';
|
||||||
|
|
||||||
|
|
|
@ -1,44 +1,45 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use stdClass;
|
||||||
|
use RuntimeException;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
use DeviceDetector\ClientHints;
|
use DeviceDetector\ClientHints;
|
||||||
use DeviceDetector\DeviceDetector;
|
use DeviceDetector\DeviceDetector;
|
||||||
|
|
||||||
class ClientInfo implements Stringable {
|
class ClientInfo implements Stringable {
|
||||||
private DeviceDetector $dd;
|
private const SERIALIZE_VERSION = 1;
|
||||||
|
|
||||||
public function __construct(DeviceDetector $dd) {
|
public function __construct(
|
||||||
$this->dd = $dd;
|
private array|bool|null $botInfo,
|
||||||
}
|
private ?array $clientInfo,
|
||||||
|
private ?array $osInfo,
|
||||||
|
private string $brandName,
|
||||||
|
private string $modelName
|
||||||
|
) {}
|
||||||
|
|
||||||
public function __toString(): string {
|
public function __toString(): string {
|
||||||
if($this->dd->isBot()) {
|
if($this->botInfo === true || is_array($this->botInfo)) {
|
||||||
$botInfo = $this->dd->getBot();
|
if($this->botInfo === true)
|
||||||
return $botInfo['name'] ?? 'an unknown bot';
|
return 'a bot';
|
||||||
|
return $this->botInfo['name'] ?? 'an unknown bot';
|
||||||
}
|
}
|
||||||
|
|
||||||
$clientInfo = $this->dd->getClient();
|
if(empty($this->clientInfo['name']))
|
||||||
if(empty($clientInfo['name']))
|
|
||||||
return 'an unknown browser';
|
return 'an unknown browser';
|
||||||
|
|
||||||
$string = $clientInfo['name'];
|
$string = $this->clientInfo['name'];
|
||||||
if(!empty($clientInfo['version']))
|
if(!empty($this->clientInfo['version']))
|
||||||
$string .= ' ' . $clientInfo['version'];
|
$string .= ' ' . $this->clientInfo['version'];
|
||||||
|
|
||||||
$osInfo = $this->dd->getOs();
|
$hasOsInfo = !empty($this->osInfo['name']);
|
||||||
$hasOsInfo = !empty($osInfo['name']);
|
$hasModelName = !empty($this->modelName);
|
||||||
|
|
||||||
$brandName = $this->dd->getBrandName();
|
|
||||||
$modelName = $this->dd->getModel();
|
|
||||||
$hasModelName = !empty($modelName);
|
|
||||||
|
|
||||||
if($hasOsInfo || $hasModelName)
|
if($hasOsInfo || $hasModelName)
|
||||||
$string .= ' on ';
|
$string .= ' on ';
|
||||||
|
|
||||||
if($hasModelName) {
|
if($hasModelName) {
|
||||||
$deviceName = trim($brandName . ' ' . $modelName);
|
$deviceName = trim($this->brandName . ' ' . $this->modelName);
|
||||||
// most naive check in the world but it works well enough for this lol
|
// most naive check in the world but it works well enough for this lol
|
||||||
$firstCharIsVowel = in_array(strtolower($deviceName[0]), ['a', 'i', 'u', 'e', 'o']);
|
$firstCharIsVowel = in_array(strtolower($deviceName[0]), ['a', 'i', 'u', 'e', 'o']);
|
||||||
$string .= ($firstCharIsVowel ? 'an' : 'a') . ' ' . $deviceName;
|
$string .= ($firstCharIsVowel ? 'an' : 'a') . ' ' . $deviceName;
|
||||||
|
@ -48,29 +49,71 @@ class ClientInfo implements Stringable {
|
||||||
if($hasModelName)
|
if($hasModelName)
|
||||||
$string .= ' running ';
|
$string .= ' running ';
|
||||||
|
|
||||||
$string .= $osInfo['name'];
|
$string .= $this->osInfo['name'];
|
||||||
if(!empty($osInfo['version']))
|
if(!empty($this->osInfo['version']))
|
||||||
$string .= ' ' . $osInfo['version'];
|
$string .= ' ' . $this->osInfo['version'];
|
||||||
if(!empty($osInfo['platform']))
|
if(!empty($this->osInfo['platform']))
|
||||||
$string .= ' (' . $osInfo['platform'] . ')';
|
$string .= ' (' . $this->osInfo['platform'] . ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $string;
|
return $string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function encode(): string {
|
||||||
|
$data = new stdClass;
|
||||||
|
$data->version = self::SERIALIZE_VERSION;
|
||||||
|
|
||||||
|
if($this->botInfo === true || is_array($this->botInfo))
|
||||||
|
$data->bot = $this->botInfo;
|
||||||
|
if($this->clientInfo !== null)
|
||||||
|
$data->client = $this->clientInfo;
|
||||||
|
if($this->osInfo !== null)
|
||||||
|
$data->os = $this->osInfo;
|
||||||
|
if($this->brandName !== '')
|
||||||
|
$data->vendor = $this->brandName;
|
||||||
|
if($this->modelName !== '')
|
||||||
|
$data->model = $this->modelName;
|
||||||
|
|
||||||
|
return json_encode($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decode(string $encoded): self {
|
||||||
|
$data = json_decode($encoded, true);
|
||||||
|
$version = $data['version'] ?? 0;
|
||||||
|
if($version < 0 || $version > self::SERIALIZE_VERSION)
|
||||||
|
throw new RuntimeException('$data does not contain a valid version argument');
|
||||||
|
|
||||||
|
return new static(
|
||||||
|
$data['bot'] ?? null,
|
||||||
|
$data['client'] ?? null,
|
||||||
|
$data['os'] ?? null,
|
||||||
|
$data['vendor'] ?? '',
|
||||||
|
$data['model'] ?? ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static function parse(array|string $serverVarsOrUserAgent): self {
|
public static function parse(array|string $serverVarsOrUserAgent): self {
|
||||||
|
static $dd = null;
|
||||||
|
$dd ??= new DeviceDetector();
|
||||||
|
|
||||||
if(is_string($serverVarsOrUserAgent)) {
|
if(is_string($serverVarsOrUserAgent)) {
|
||||||
$userAgent = $serverVarsOrUserAgent;
|
$dd->setUserAgent($serverVarsOrUserAgent);
|
||||||
$clientHints = null;
|
|
||||||
} else {
|
} else {
|
||||||
$userAgent = array_key_exists('HTTP_USER_AGENT', $serverVarsOrUserAgent)
|
$dd->setUserAgent(
|
||||||
? $serverVarsOrUserAgent['HTTP_USER_AGENT'] : '';
|
array_key_exists('HTTP_USER_AGENT', $serverVarsOrUserAgent)
|
||||||
$clientHints = ClientHints::factory($serverVarsOrUserAgent);
|
? $serverVarsOrUserAgent['HTTP_USER_AGENT'] : ''
|
||||||
|
);
|
||||||
|
$dd->setClientHints(ClientHints::factory($serverVarsOrUserAgent));
|
||||||
}
|
}
|
||||||
|
|
||||||
$dd = new DeviceDetector($userAgent, $clientHints);
|
|
||||||
$dd->parse();
|
$dd->parse();
|
||||||
|
|
||||||
return new static($dd);
|
return new static(
|
||||||
|
$dd->getBot(),
|
||||||
|
$dd->getClient(),
|
||||||
|
$dd->getOs(),
|
||||||
|
$dd->getBrandName(),
|
||||||
|
$dd->getModel()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,13 +13,14 @@ class UserLoginAttempt {
|
||||||
private $attempt_country = 'XX';
|
private $attempt_country = 'XX';
|
||||||
private $attempt_created = null;
|
private $attempt_created = null;
|
||||||
private $attempt_user_agent = '';
|
private $attempt_user_agent = '';
|
||||||
|
private $attempt_client_info = '';
|
||||||
|
|
||||||
private $user = null;
|
private $user = null;
|
||||||
private $userLookedUp = false;
|
private $userLookedUp = false;
|
||||||
|
|
||||||
public const TABLE = 'login_attempts';
|
public const TABLE = 'login_attempts';
|
||||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||||
private const SELECT = '%1$s.`user_id`, %1$s.`attempt_success`, %1$s.`attempt_country`, %1$s.`attempt_user_agent`'
|
private const SELECT = '%1$s.`user_id`, %1$s.`attempt_success`, %1$s.`attempt_country`, %1$s.`attempt_user_agent`, %1$s.`attempt_client_info`'
|
||||||
. ', INET6_NTOA(%1$s.`attempt_ip`) AS `attempt_ip`'
|
. ', INET6_NTOA(%1$s.`attempt_ip`) AS `attempt_ip`'
|
||||||
. ', UNIX_TIMESTAMP(%1$s.`attempt_created`) AS `attempt_created`';
|
. ', UNIX_TIMESTAMP(%1$s.`attempt_created`) AS `attempt_created`';
|
||||||
|
|
||||||
|
@ -58,8 +59,8 @@ class UserLoginAttempt {
|
||||||
public function getUserAgent(): string {
|
public function getUserAgent(): string {
|
||||||
return $this->attempt_user_agent;
|
return $this->attempt_user_agent;
|
||||||
}
|
}
|
||||||
public function getClientString(): string {
|
public function getClientInfo(): ClientInfo {
|
||||||
return (string)ClientInfo::parse($this->attempt_user_agent);
|
return ClientInfo::decode($this->attempt_client_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function remaining(string $remoteAddr): int {
|
public static function remaining(string $remoteAddr): int {
|
||||||
|
@ -78,18 +79,21 @@ class UserLoginAttempt {
|
||||||
string $countryCode,
|
string $countryCode,
|
||||||
bool $success,
|
bool $success,
|
||||||
?User $user = null,
|
?User $user = null,
|
||||||
string $userAgent = null
|
string $userAgent = null,
|
||||||
|
?ClientInfo $clientInfo = null
|
||||||
): void {
|
): void {
|
||||||
$userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') ?? '';
|
$userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') ?? '';
|
||||||
|
$clientInfo ??= ClientInfo::parse($_SERVER);
|
||||||
|
|
||||||
$createLog = DB::prepare(
|
$createLog = DB::prepare(
|
||||||
'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `attempt_success`, `attempt_ip`, `attempt_country`, `attempt_user_agent`)'
|
'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `attempt_success`, `attempt_ip`, `attempt_country`, `attempt_user_agent`, `attempt_client_info`)'
|
||||||
. ' VALUES (:user, :success, INET6_ATON(:ip), :country, :user_agent)'
|
. ' VALUES (:user, :success, INET6_ATON(:ip), :country, :user_agent, :client_info)'
|
||||||
) ->bind('user', $user === null ? null : $user->getId()) // this null situation should never ever happen but better safe than sorry !
|
) ->bind('user', $user === null ? null : $user->getId()) // this null situation should never ever happen but better safe than sorry !
|
||||||
->bind('success', $success ? 1 : 0)
|
->bind('success', $success ? 1 : 0)
|
||||||
->bind('ip', $remoteAddr)
|
->bind('ip', $remoteAddr)
|
||||||
->bind('country', $countryCode)
|
->bind('country', $countryCode)
|
||||||
->bind('user_agent', $userAgent)
|
->bind('user_agent', $userAgent)
|
||||||
|
->bind('client_info', $clientInfo->encode())
|
||||||
->execute();
|
->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ class UserSession {
|
||||||
private $session_ip = '::1';
|
private $session_ip = '::1';
|
||||||
private $session_ip_last = null;
|
private $session_ip_last = null;
|
||||||
private $session_user_agent = '';
|
private $session_user_agent = '';
|
||||||
|
private $session_client_info = '';
|
||||||
private $session_country = 'XX';
|
private $session_country = 'XX';
|
||||||
private $session_expires = null;
|
private $session_expires = null;
|
||||||
private $session_expires_bump = 1;
|
private $session_expires_bump = 1;
|
||||||
|
@ -32,7 +33,7 @@ class UserSession {
|
||||||
|
|
||||||
public const TABLE = 'sessions';
|
public const TABLE = 'sessions';
|
||||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||||
private const SELECT = '%1$s.`session_id`, %1$s.`user_id`, %1$s.`session_key`, %1$s.`session_user_agent`, %1$s.`session_country`, %1$s.`session_expires_bump`'
|
private const SELECT = '%1$s.`session_id`, %1$s.`user_id`, %1$s.`session_key`, %1$s.`session_user_agent`, %1$s.`session_client_info`, %1$s.`session_country`, %1$s.`session_expires_bump`'
|
||||||
. ', INET6_NTOA(%1$s.`session_ip`) AS `session_ip`'
|
. ', INET6_NTOA(%1$s.`session_ip`) AS `session_ip`'
|
||||||
. ', INET6_NTOA(%1$s.`session_ip_last`) AS `session_ip_last`'
|
. ', INET6_NTOA(%1$s.`session_ip_last`) AS `session_ip_last`'
|
||||||
. ', UNIX_TIMESTAMP(%1$s.`session_created`) AS `session_created`'
|
. ', UNIX_TIMESTAMP(%1$s.`session_created`) AS `session_created`'
|
||||||
|
@ -74,8 +75,8 @@ class UserSession {
|
||||||
public function getUserAgent(): string {
|
public function getUserAgent(): string {
|
||||||
return $this->session_user_agent;
|
return $this->session_user_agent;
|
||||||
}
|
}
|
||||||
public function getClientString(): string {
|
public function getClientInfo(): ClientInfo {
|
||||||
return (string)ClientInfo::parse($this->session_user_agent);
|
return ClientInfo::decode($this->session_client_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCountry(): string {
|
public function getCountry(): string {
|
||||||
|
@ -170,18 +171,26 @@ class UserSession {
|
||||||
->execute();
|
->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function create(User $user, string $remoteAddr, string $countryCode, ?string $userAgent = null, ?string $token = null): self {
|
public static function create(
|
||||||
|
User $user,
|
||||||
|
string $remoteAddr,
|
||||||
|
string $countryCode,
|
||||||
|
?string $userAgent = null,
|
||||||
|
?ClientInfo $clientInfo = null
|
||||||
|
): self {
|
||||||
$userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') ?? '';
|
$userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') ?? '';
|
||||||
$token = $token ?? self::generateToken();
|
$clientInfo ??= ClientInfo::parse($_SERVER);
|
||||||
|
$token = self::generateToken();
|
||||||
|
|
||||||
$sessionId = DB::prepare(
|
$sessionId = DB::prepare(
|
||||||
'INSERT INTO `' . DB::PREFIX . self::TABLE . '`'
|
'INSERT INTO `' . DB::PREFIX . self::TABLE . '`'
|
||||||
. ' (`user_id`, `session_ip`, `session_country`, `session_user_agent`, `session_key`, `session_created`, `session_expires`)'
|
. ' (`user_id`, `session_ip`, `session_country`, `session_user_agent`, `session_client_info`, `session_key`, `session_created`, `session_expires`)'
|
||||||
. ' VALUES (:user, INET6_ATON(:remote_addr), :country, :user_agent, :token, NOW(), NOW() + INTERVAL :expires SECOND)'
|
. ' VALUES (:user, INET6_ATON(:remote_addr), :country, :user_agent, :client_info, :token, NOW(), NOW() + INTERVAL :expires SECOND)'
|
||||||
) ->bind('user', $user->getId())
|
) ->bind('user', $user->getId())
|
||||||
->bind('remote_addr', $remoteAddr)
|
->bind('remote_addr', $remoteAddr)
|
||||||
->bind('country', $countryCode)
|
->bind('country', $countryCode)
|
||||||
->bind('user_agent', $userAgent)
|
->bind('user_agent', $userAgent)
|
||||||
|
->bind('client_info', $clientInfo->encode())
|
||||||
->bind('token', $token)
|
->bind('token', $token)
|
||||||
->bind('expires', self::LIFETIME)
|
->bind('expires', self::LIFETIME)
|
||||||
->executeGetId();
|
->executeGetId();
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<div class="flag flag--{{ session.country|lower }} settings__session__flag" title="{{ session.countryName }}">{{ session.country }}</div>
|
<div class="flag flag--{{ session.country|lower }} settings__session__flag" title="{{ session.countryName }}">{{ session.country }}</div>
|
||||||
|
|
||||||
<div class="settings__session__description">
|
<div class="settings__session__description">
|
||||||
{{ session.clientString }}
|
{{ session.clientInfo }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="settings__session__actions" method="post" action="{{ url('settings-sessions') }}">
|
<form class="settings__session__actions" method="post" action="{{ url('settings-sessions') }}">
|
||||||
|
@ -160,7 +160,7 @@
|
||||||
<div class="flag flag--{{ attempt.country|lower }} settings__login-attempt__flag" title="{{ attempt.countryName }}">{{ attempt.country }}</div>
|
<div class="flag flag--{{ attempt.country|lower }} settings__login-attempt__flag" title="{{ attempt.countryName }}">{{ attempt.country }}</div>
|
||||||
|
|
||||||
<div class="settings__login-attempt__description">
|
<div class="settings__login-attempt__description">
|
||||||
{{ attempt.clientString }}
|
{{ attempt.clientInfo }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue