Compare commits

..

9 Commits

Author SHA1 Message Date
slawkens
058e80cfc6 Update Monsters.php 2026-02-15 02:02:07 +01:00
slawkens
980d5cb5d4 Update Items.php 2026-02-15 02:02:05 +01:00
slawkens
9146a0e53c Parse last item 2026-02-15 02:01:57 +01:00
slawkens
ad1d069b1f Copilot fixes 2026-02-15 01:57:26 +01:00
slawkens
4bc3923996 glob is not needed here 2026-02-15 01:20:11 +01:00
slawkens
8809467fc6 Delete debug output 2026-02-15 01:19:58 +01:00
slawkens
97ee430a91 Forgot about composer 2026-02-15 01:08:27 +01:00
slawkens
fc9d6b2b91 [WIP] Initial Blacktek support - groups, vocations, items
items.toml loading through custom parser - no .toml reader that I tested can read it
2026-02-15 00:54:18 +01:00
slawkens
4c3f877091 Update CHANGELOG-2.x.md 2026-02-12 18:21:19 +01:00
63 changed files with 1282 additions and 2060 deletions

View File

@@ -3,7 +3,7 @@
### Added
* Add an "access" option to Menus (#340)
* Possibility to hide menus for unauthorized users
* Add the possibility to fetch skills in the getTopPlayers function (#347)
* Add the possibility to fetch skills, balance and frags in the getTopPlayers function (#347)
### Changed
* Better handling of vocations: (#345)

View File

@@ -27,7 +27,7 @@ if (version_compare(phpversion(), '8.1', '<')) die('PHP version 8.1 or higher is
const MYAAC = true;
const MYAAC_VERSION = '2.0-dev';
const DATABASE_VERSION = 51;
const DATABASE_VERSION = 50;
const TABLE_PREFIX = 'myaac_';
define('START_TIME', microtime(true));
define('MYAAC_OS', stripos(PHP_OS, 'WIN') === 0 ? 'WINDOWS' : (strtoupper(PHP_OS) === 'DARWIN' ? 'MAC' : 'LINUX'));

View File

@@ -20,7 +20,7 @@
"filp/whoops": "^2.15",
"maximebf/debugbar": "1.*",
"guzzlehttp/guzzle": "7.9.3",
"spomky-labs/otphp": "^11.3"
"devium/toml": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^1.10"

846
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,6 @@ CREATE TABLE IF NOT EXISTS `myaac_account_actions`
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `myaac_account_email_codes`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`account_id` int NOT NULL,
`code` varchar(6) NOT NULL,
`created_at` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `myaac_account_emails_verify`
(
`id` int NOT NULL AUTO_INCREMENT,

View File

@@ -98,16 +98,6 @@ if(!$db->hasColumn('accounts', 'web_flags')) {
success($locale['step_database_adding_field'] . ' accounts.web_flags...');
}
if(!$db->hasColumn('accounts', '2fa_type')) {
if(query("ALTER TABLE `accounts` ADD `2fa_type` tinyint NOT NULL DEFAULT 0 AFTER `web_flags`;"))
success($locale['step_database_adding_field'] . ' accounts.2fa_type...');
}
if(!$db->hasColumn('accounts', '2fa_secret')) {
if(query("ALTER TABLE `accounts` ADD `2fa_secret` varchar(16) NOT NULL DEFAULT '' AFTER `2fa_type`;"))
success($locale['step_database_adding_field'] . ' accounts.2fa_secret...');
}
if(!$db->hasColumn('accounts', 'email_verified')) {
if(query("ALTER TABLE `accounts` ADD `email_verified` TINYINT(1) NOT NULL DEFAULT 0 AFTER `web_flags`;"))
success($locale['step_database_adding_field'] . ' accounts.email_verified...');

104
login.php
View File

@@ -5,7 +5,6 @@ use MyAAC\Models\PlayerOnline;
use MyAAC\Models\Account;
use MyAAC\Models\Player;
use MyAAC\RateLimit;
use MyAAC\TwoFactorAuth\TwoFactorAuth;
require_once 'common.php';
require_once SYSTEM . 'functions.php';
@@ -13,7 +12,7 @@ require_once SYSTEM . 'init.php';
require_once SYSTEM . 'status.php';
# error function
function sendError($message, $code = 3) {
function sendError($message, $code = 3){
$ret = [];
$ret['errorCode'] = $code;
$ret['errorMessage'] = $message;
@@ -109,18 +108,17 @@ switch ($action) {
case 'login':
$ip = configLua('ip');
$port = configLua('gameProtocolPort');
$port = $config['lua']['gameProtocolPort'];
// default world info
$world = [
'id' => 0,
'name' => $config['lua']['serverName'],
'externaladdress' => $ip,
'externaladdress' => $config['lua']['ip'],
'externalport' => $port,
'externaladdressprotected' => $ip,
'externaladdressprotected' => $config['lua']['ip'],
'externalportprotected' => $port,
'externaladdressunprotected' => $ip,
'externaladdressunprotected' => $config['lua']['ip'],
'externalportunprotected' => $port,
'previewstate' => 0,
'location' => 'BRA', // BRA, EUR, USA
@@ -135,12 +133,13 @@ switch ($action) {
$inputEmail = $request->email ?? false;
$inputAccountName = $request->accountname ?? false;
$inputToken = $request->token ?? false;
$account = Account::query();
if ($inputEmail) { // login by email
if ($inputEmail != false) { // login by email
$account->where('email', $inputEmail);
}
else if($inputAccountName) { // login by account name
else if($inputAccountName != false) { // login by account name
$account->where('name', $inputAccountName);
}
@@ -152,14 +151,13 @@ switch ($action) {
$limiter->load();
$ban_msg = 'A wrong account, password or secret has been entered ' . setting('core.account_login_attempts_limit') . ' times in a row. You are unable to log into your account for the next ' . setting('core.account_login_ban_time') . ' minutes. Please wait.';
if (!$account) {
$limiter->increment($ip);
if ($limiter->exceeded($ip)) {
sendError($ban_msg);
}
sendError(($inputEmail ? 'Email' : 'Account name') . ' or password is not correct.');
sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.');
}
$current_password = encrypt((USE_ACCOUNT_SALT ? $account->salt : '') . $request->password);
@@ -169,30 +167,32 @@ switch ($action) {
sendError($ban_msg);
}
sendError(($inputEmail ? 'Email' : 'Account name') . ' or password is not correct.');
sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.');
}
$twoFactorAuth = TwoFactorAuth::getInstance($account->id);
$accountHasSecret = false;
if (fieldExist('secret', 'accounts')) {
$accountSecret = $account->secret;
if ($accountSecret != null && $accountSecret != '') {
$accountHasSecret = true;
if ($inputToken === false) {
$limiter->increment($ip);
if ($limiter->exceeded($ip)) {
sendError($ban_msg);
}
sendError('Submit a valid two-factor authentication token.', 6);
} else {
require_once LIBS . 'rfc6238.php';
if (TokenAuth6238::verify($accountSecret, $inputToken) !== true) {
$limiter->increment($ip);
if ($limiter->exceeded($ip)) {
sendError($ban_msg);
}
$code = '';
if ($twoFactorAuth->isActive()) {
if ($twoFactorAuth->getAuthType() === TwoFactorAuth::TYPE_EMAIL) {
$code = $request->emailcode ?? false;
sendError('Two-factor authentication failed, token is wrong.', 6);
}
}
}
else if ($twoFactorAuth->getAuthType() === TwoFactorAuth::TYPE_APP) {
$code = $request->token ?? false;
}
}
$error = '';
$errorCode = 6;
if (!$twoFactorAuth->processClientLogin($code, $error, $errorCode)) {
$limiter->increment($ip);
if ($limiter->exceeded($ip)) {
sendError($ban_msg);
}
sendError($error, $errorCode);
}
$limiter->reset($ip);
@@ -220,6 +220,46 @@ switch ($action) {
}
}
/*
* not needed anymore?
if (fieldExist('premdays', 'accounts') && fieldExist('lastday', 'accounts')) {
$save = false;
$timeNow = time();
$premDays = $account->premdays;
$lastDay = $account->lastday;
$lastLogin = $lastDay;
if ($premDays != 0 && $premDays != PHP_INT_MAX) {
if ($lastDay == 0) {
$lastDay = $timeNow;
$save = true;
} else {
$days = (int)(($timeNow - $lastDay) / 86400);
if ($days > 0) {
if ($days >= $premDays) {
$premDays = 0;
$lastDay = 0;
} else {
$premDays -= $days;
$reminder = ($timeNow - $lastDay) % 86400;
$lastDay = $timeNow - $reminder;
}
$save = true;
}
}
} else if ($lastDay != 0) {
$lastDay = 0;
$save = true;
}
if ($save) {
$account->premdays = $premDays;
$account->lastday = $lastDay;
$account->save();
}
}
*/
$worlds = [$world];
$playdata = compact('worlds', 'characters');
@@ -228,7 +268,7 @@ switch ($action) {
if (!fieldExist('istutorial', 'players')) {
$sessionKey .= "\n";
}
$sessionKey .= ($twoFactorAuth->isActive() && strlen($account->{'2fa_secret'}) > 5) ? $account->{'2fa_secret'} : '';
$sessionKey .= ($accountHasSecret && strlen($accountSecret) > 5) ? $inputToken : '';
// this is workaround to distinguish between TFS 1.x and otservbr
// TFS 1.x requires the number in session key

View File

@@ -4,6 +4,7 @@ require __DIR__ . '/system/libs/pot/OTS.php';
$ots = POT::getInstance();
require __DIR__ . '/system/libs/pot/InvitesDriver.php';
require __DIR__ . '/system/libs/rfc6238.php';
require __DIR__ . '/common.php';
const ACTION = '';

View File

@@ -14,7 +14,7 @@ use MyAAC\CsrfToken;
use MyAAC\Hooks;
use MyAAC\Plugins;
use MyAAC\Models\Town;
use MyAAC\Server\XML\Vocations;
use MyAAC\Server\Vocations;
use MyAAC\Settings;
defined('MYAAC') or die('Direct access not allowed!');

View File

@@ -736,11 +736,17 @@ class OTS_Account extends OTS_Row_DAO implements IteratorAggregate, Countable
*/
public function setCustomField($field, $value)
{
if( !isset($this->data['id']) ) {
if( !isset($this->data['id']) )
{
throw new E_OTS_NotLoaded();
}
AccountModel::where('id', $this->data['id'])->update([$field => $value]);
// quotes value for SQL query
if(!( is_int($value) || is_float($value) ))
{
$value = $this->db->quote($value);
}
$this->db->exec('UPDATE ' . $this->db->tableName('accounts') . ' SET ' . $this->db->fieldName($field) . ' = ' . $value . ' WHERE ' . $this->db->fieldName('id') . ' = ' . $this->data['id']);
}
/**

View File

@@ -8,7 +8,7 @@
* @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU Lesser General Public License, Version 3
*/
use MyAAC\Cache\Cache;
use MyAAC\Server\Groups;
/**
* List of groups.
@@ -47,73 +47,9 @@ class OTS_Groups_List implements IteratorAggregate, Countable
return;
}
if(!isset($file[0]))
{
global $config;
$file = $config['data_path'] . 'XML/groups.xml';
}
if(!@file_exists($file)) {
error('Error: Cannot load groups.xml. More info in system/logs/error.log file.');
log_append('error.log', '[OTS_Groups_List.php] Fatal error: Cannot load groups.xml (' . $file . '). It doesnt exist.');
return;
}
$cache = Cache::getInstance();
$data = array();
if($cache->enabled())
{
$tmp = '';
if($cache->fetch('groups', $tmp))
$data = unserialize($tmp);
else
{
$groups = new DOMDocument();
if(!@$groups->load($file)) {
error('Error: Cannot load groups.xml. More info in system/logs/error.log file.');
log_append('error.log', '[OTS_Groups_List.php] Fatal error: Cannot load groups.xml (' . $file . '). Error: ' . print_r(error_get_last(), true));
return;
}
// loads groups
foreach( $groups->getElementsByTagName('group') as $group)
{
$data[$group->getAttribute('id')] = array(
'id' => $group->getAttribute('id'),
'name' => $group->getAttribute('name'),
'access' => $group->getAttribute('access')
);
}
$cache->set('groups', serialize($data), 120);
}
foreach($data as $id => $info)
$this->groups[ $id ] = new OTS_Group($info);
}
else
{
// loads DOM document
$groups = new DOMDocument();
if(!@$groups->load($file)) {
error('Error: Cannot load groups.xml. More info in system/logs/error.log file.');
log_append('error.log', '[OTS_Groups_List.php] Fatal error: Cannot load groups.xml (' . $file . '). Error: ' . print_r(error_get_last(), true));
return;
}
// loads groups
foreach($groups->getElementsByTagName('group') as $group)
{
$data[$group->getAttribute('id')] = array(
'id' => $group->getAttribute('id'),
'name' => $group->getAttribute('name'),
'access' => $group->getAttribute('access')
);
$this->groups[ $group->getAttribute('id') ] = new OTS_Group($data[$group->getAttribute('id')]);
//echo $this->getGroup(1)->getName();
}
$groups = new Groups();
foreach($groups->getGroups() as $id => $info) {
$this->groups[$id] = new OTS_Group($info);
}
}

284
system/libs/rfc6238.php Normal file
View File

@@ -0,0 +1,284 @@
<?php
/** https://github.com/Voronenko/PHPOTP/blob/08cda9cb9c30b7242cf0b3a9100a6244a2874927/code/base32static.php
* Encode in Base32 based on RFC 4648.
* Requires 20% more space than base64
* Great for case-insensitive filesystems like Windows and URL's (except for = char which can be excluded using the pad option for urls)
*
* @package default
* @author Bryan Ruiz
**/
class Base32Static {
private static $map = array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=' // padding character
);
private static $flippedMap = array(
'A'=>'0', 'B'=>'1', 'C'=>'2', 'D'=>'3', 'E'=>'4', 'F'=>'5', 'G'=>'6', 'H'=>'7',
'I'=>'8', 'J'=>'9', 'K'=>'10', 'L'=>'11', 'M'=>'12', 'N'=>'13', 'O'=>'14', 'P'=>'15',
'Q'=>'16', 'R'=>'17', 'S'=>'18', 'T'=>'19', 'U'=>'20', 'V'=>'21', 'W'=>'22', 'X'=>'23',
'Y'=>'24', 'Z'=>'25', '2'=>'26', '3'=>'27', '4'=>'28', '5'=>'29', '6'=>'30', '7'=>'31'
);
/**
* Use padding false when encoding for urls
*
* @return base32 encoded string
* @author Bryan Ruiz
**/
public static function encode($input, $padding = true) {
if(empty($input)) return "";
$input = str_split($input);
$binaryString = "";
for($i = 0; $i < count($input); $i++) {
$binaryString .= str_pad(base_convert(ord($input[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
}
$fiveBitBinaryArray = str_split($binaryString, 5);
$base32 = "";
$i=0;
while($i < count($fiveBitBinaryArray)) {
$base32 .= self::$map[base_convert(str_pad($fiveBitBinaryArray[$i], 5,'0'), 2, 10)];
$i++;
}
if($padding && ($x = strlen($binaryString) % 40) != 0) {
if($x == 8) $base32 .= str_repeat(self::$map[32], 6);
else if($x == 16) $base32 .= str_repeat(self::$map[32], 4);
else if($x == 24) $base32 .= str_repeat(self::$map[32], 3);
else if($x == 32) $base32 .= self::$map[32];
}
return $base32;
}
public static function decode($input) {
if(empty($input)) return;
$paddingCharCount = substr_count($input, self::$map[32]);
$allowedValues = array(6,4,3,1,0);
if(!in_array($paddingCharCount, $allowedValues)) return false;
for($i=0; $i<4; $i++){
if($paddingCharCount == $allowedValues[$i] &&
substr($input, -($allowedValues[$i])) != str_repeat(self::$map[32], $allowedValues[$i])) return false;
}
$input = str_replace('=','', $input);
$input = str_split($input);
$binaryString = "";
for($i=0; $i < count($input); $i = $i+8) {
$x = "";
if(!in_array($input[$i], self::$map)) return false;
for($j=0; $j < 8; $j++) {
$x .= str_pad(base_convert(@self::$flippedMap[@$input[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
}
$eightBits = str_split($x, 8);
for($z = 0; $z < count($eightBits); $z++) {
$binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
}
}
return $binaryString;
}
}
// http://www.faqs.org/rfcs/rfc6238.html
// https://github.com/Voronenko/PHPOTP/blob/08cda9cb9c30b7242cf0b3a9100a6244a2874927/code/rfc6238.php
// Local changes: http -> https, consistent indentation, 200x200 -> 300x300 QR image size, PHP end tag
class TokenAuth6238 {
/**
* verify
*
* @param string $secretkey Secret clue (base 32).
* @return bool True if success, false if failure
*/
public static function verify($secretkey, $code, $rangein30s = 3) {
$key = base32static::decode($secretkey);
$unixtimestamp = time()/30;
for($i=-($rangein30s); $i<=$rangein30s; $i++) {
$checktime = (int)($unixtimestamp+$i);
$thiskey = self::oath_hotp($key, $checktime);
if ((int)$code == self::oath_truncate($thiskey,6)) {
return true;
}
}
return false;
}
public static function getTokenCode($secretkey,$rangein30s = 3) {
$result = "";
$key = base32static::decode($secretkey);
$unixtimestamp = time()/30;
for($i=-($rangein30s); $i<=$rangein30s; $i++) {
$checktime = (int)($unixtimestamp+$i);
$thiskey = self::oath_hotp($key, $checktime);
$result = $result." # ".self::oath_truncate($thiskey,6);
}
return $result;
}
public static function getTokenCodeDebug($secretkey,$rangein30s = 3) {
$result = "";
print "<br/>SecretKey: $secretkey <br/>";
$key = base32static::decode($secretkey);
print "Key(base 32 decode): $key <br/>";
$unixtimestamp = time()/30;
print "UnixTimeStamp (time()/30): $unixtimestamp <br/>";
for($i=-($rangein30s); $i<=$rangein30s; $i++) {
$checktime = (int)($unixtimestamp+$i);
print "Calculating oath_hotp from (int)(unixtimestamp +- 30sec offset): $checktime basing on secret key<br/>";
$thiskey = self::oath_hotp($key, $checktime, true);
print "======================================================<br/>";
print "CheckTime: $checktime oath_hotp:".$thiskey."<br/>";
$result = $result." # ".self::oath_truncate($thiskey,6,true);
}
return $result;
}
public static function getBarCodeUrl($username, $domain, $secretkey, $issuer) {
$url = "https://chart.apis.google.com/chart";
$url = $url."?chs=300x300&chld=M|0&cht=qr&chl=otpauth://totp/";
$url = $url.$username . "@" . $domain . "%3Fsecret%3D" . $secretkey . '%26issuer%3D' . rawurlencode($issuer);
return $url;
}
public static function generateRandomClue($length = 16) {
$b32 = "234567QWERTYUIOPASDFGHJKLZXCVBNM";
$s = "";
for ($i = 0; $i < $length; $i++)
$s .= $b32[rand(0,31)];
return $s;
}
private static function hotp_tobytestream($key) {
$result = array();
$last = strlen($key);
for ($i = 0; $i < $last; $i = $i + 2) {
$x = $key[$i] + $key[$i + 1];
$x = strtoupper($x);
$x = hexdec($x);
$result = $result.chr($x);
}
return $result;
}
private static function oath_hotp ($key, $counter, $debug=false) {
$result = "";
$orgcounter = $counter;
$cur_counter = array(0,0,0,0,0,0,0,0);
if ($debug) {
print "Packing counter $counter (".dechex($counter).")into binary string - pay attention to hex representation of key and binary representation<br/>";
}
for($i=7;$i>=0;$i--) { // C for unsigned char, * for repeating to the end of the input data
$cur_counter[$i] = pack ('C*', $counter);
if ($debug) {
print $cur_counter[$i]."(".dechex(ord($cur_counter[$i])).")"." from $counter <br/>";
}
$counter = $counter >> 8;
}
if ($debug) {
foreach ($cur_counter as $char) {
print ord($char) . " ";
}
print "<br/>";
}
$binary = implode($cur_counter);
// Pad to 8 characters
str_pad($binary, 8, chr(0), STR_PAD_LEFT);
if ($debug) {
print "Prior to HMAC calculation pad with zero on the left until 8 characters.<br/>";
print "Calculate sha1 HMAC(Hash-based Message Authentication Code http://en.wikipedia.org/wiki/HMAC).<br/>";
print "hash_hmac ('sha1', $binary, $key)<br/>";
}
$result = hash_hmac ('sha1', $binary, $key);
if ($debug) {
print "Result: $result <br/>";
}
return $result;
}
private static function oath_truncate($hash, $length = 6, $debug=false) {
$result="";
// Convert to dec
if($debug) {
print "converting hex hash into characters<br/>";
}
$hashcharacters = str_split($hash,2);
if($debug) {
print_r($hashcharacters);
print "<br/>and convert to decimals:<br/>";
}
for ($j=0; $j<count($hashcharacters); $j++) {
$hmac_result[]=hexdec($hashcharacters[$j]);
}
if($debug) {
print_r($hmac_result);
}
// http://php.net/manual/ru/function.hash-hmac.php
// adopted from brent at thebrent dot net 21-May-2009 08:17 comment
$offset = $hmac_result[19] & 0xf;
if($debug) {
print "Calculating offset as 19th element of hmac:".$hmac_result[19]."<br/>";
print "offset:".$offset;
}
$result = (
(($hmac_result[$offset+0] & 0x7f) << 24 ) |
(($hmac_result[$offset+1] & 0xff) << 16 ) |
(($hmac_result[$offset+2] & 0xff) << 8 ) |
($hmac_result[$offset+3] & 0xff)
) % pow(10,$length);
return $result;
}
}

View File

@@ -1,8 +0,0 @@
CREATE TABLE `myaac_account_email_codes`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`account_id` int NOT NULL,
`code` varchar(6) NOT NULL,
`created_at` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

View File

@@ -1,36 +0,0 @@
<?php
// 2fa
// add the myaac_account_email_codes
/**
* @var OTS_DB_MySQL $db
*/
$up = function () use ($db) {
if (!$db->hasColumn('accounts', '2fa_type')) {
$db->addColumn('accounts', '2fa_type', "tinyint NOT NULL DEFAULT 0 AFTER `web_flags`");
}
if (!$db->hasColumn('accounts', '2fa_secret')) {
$db->addColumn('accounts', '2fa_secret', "varchar(16) NOT NULL DEFAULT '' AFTER `2fa_type`");
}
// add myaac_account_email_codes table
if (!$db->hasTable(TABLE_PREFIX . 'account_email_codes')) {
$db->exec(file_get_contents(__DIR__ . '/51-account_email_codes.sql'));
}
};
$down = function () use ($db) {
if ($db->hasColumn('accounts', '2fa_type')) {
$db->dropColumn('accounts', '2fa_type');
}
if ($db->hasColumn('accounts', '2fa_secret')) {
$db->dropColumn('accounts', '2fa_secret');
}
if ($db->hasTable(TABLE_PREFIX . 'account_email_codes')) {
$db->dropTable(TABLE_PREFIX . 'account_email_codes');
}
};

View File

@@ -1,26 +0,0 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';
if (!isRequestMethod('post')) {
error('This page cannot be accessed directly.');
return;
}
if (!$account_logged->isLoaded()) {
error('Account not found!');
return;
}
if (!$twoFactorAuth->isActive($twoFactorAuth::TYPE_APP)) {
error("Your account does not have Two Factor App Authentication enabled.");
return;
}
$twoFactorAuth->disable();
$twig->display('success.html.twig', [
'title' => 'Disabled',
'description' => 'Two Factor App Authentication has been disabled.'
]);

View File

@@ -1,105 +0,0 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
use MyAAC\TwoFactorAuth\TwoFactorAuth;
require __DIR__ . '/../base.php';
if ($twoFactorAuth->isActive()) {
$errors[] = 'Two-factor authentication is already enabled on your account.';
$twig->display('error_box.html.twig', ['errors' => $errors]);
return;
}
$explodeRecoveryKey = explode('-', $account_logged->getCustomField('key'));
$newRecoveryKeyFormat = (count($explodeRecoveryKey) == 4);
if (ACTION == 'request') {
if ($newRecoveryKeyFormat) {
$key = $_POST['key1'] . '-' . $_POST['key2'] . '-' . $_POST['key3'] . '-' . $_POST['key4'];
}
else {
$key = $_POST['key'];
}
$accountKey = $account_logged->getCustomField('key');
if (!empty($key) && $key == $accountKey) {
$secret = getSession('2fa_secret');
if ($secret === null) {
$secret = generateRandom2faSecret();
setSession('2fa_secret', $secret);
}
$twoFactorAuth->appDisplayEnable($secret);
return;
}
else {
if (empty($key)) {
$errors[] = 'Please enter the recovery key!';
}
else {
$errors[] = 'Invalid recovery key!';
}
}
}
if (ACTION == 'link') {
$secret = getSession('2fa_secret');
if ($secret === null) {
$twig->display('error_box.html.twig', ['errors' => ['Secret not set. Go back and try again.']]);
return;
}
$authCode = $_POST['auth-code'] ?? '';
if (!empty($authCode)) {
$otp = $twoFactorAuth->appInitTOTP($secret);
if (!$otp->verify($authCode)) {
$errors = ['Token is invalid!'];
$twig->display('error_box.html.twig', ['errors' => $errors]);
$twoFactorAuth->appDisplayEnable($secret, $otp, $errors);
return;
}
if ($db->hasColumn('accounts', 'secret')) {
$account_logged->setCustomField('secret', $secret);
}
$account_logged->setCustomField('2fa_secret', $secret);
$twoFactorAuth->enable(TwoFactorAuth::TYPE_APP);
$twig->display('success.html.twig',
[
'title' => 'Authenticator App Connected',
'description' => 'You successfully connected your Tibia account to an authenticator app.'
]
);
return;
}
else {
$errors = ['You have to enter the code generated by the authenticator!'];
$twig->display('error_box.html.twig', ['errors' => $errors]);
$twoFactorAuth->appDisplayEnable($secret, null, $errors);
return;
}
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', ['errors' => $errors]);
}
$twig->display('account/2fa/app/enable.warning.html.twig',
[
'newRecoveryKeyFormat' => $newRecoveryKeyFormat,
'errors' => $errors,
]
);

View File

@@ -1,41 +0,0 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
use MyAAC\TwoFactorAuth\TwoFactorAuth;
csrfProtect();
$title = 'Two Factor Authentication';
/**
* @var OTS_Account $account_logged
*/
$code = $_REQUEST['auth-code'] ?? '';
if (!$account_logged->isLoaded()) {
$current_session = getSession('account');
if($current_session) {
$account_logged = new OTS_Account();
$account_logged->load($current_session);
}
}
$twoFactorAuth = TwoFactorAuth::getInstance($account_logged);
$twig->addGlobal('account_logged', $account_logged);
/**
* Took from ZnoteAAC
* @author Znote
*/
function generateRandom2faSecret($length = 16): string
{
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}

View File

@@ -1,34 +0,0 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';
if ((!setting('core.mail_enabled'))) {
$twig->display('error_box.html.twig', ['errors' => ['Account Two-Factor E-Mail Authentication disabled.']]);
return;
}
if (!isRequestMethod('post')) {
error('This page cannot be accessed directly.');
return;
}
if (!$account_logged->isLoaded()) {
error('Account not found!');
return;
}
if (!$twoFactorAuth->isActive($twoFactorAuth::TYPE_EMAIL)) {
error("Your account does not have Two Factor E-Mail Authentication enabled.");
return;
}
$twoFactorAuth->disable();
$twoFactorAuth->deleteOldCodes();
$twig->display('success.html.twig',
[
'title' => 'Email Code Authentication Disabled',
'description' => 'You have successfully <strong>disabled</strong> the <b>Email Code Authentication</b> for your account.'
]
);

View File

@@ -1,51 +0,0 @@
<?php
use MyAAC\TwoFactorAuth\TwoFactorAuth;
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';
if ((!setting('core.mail_enabled'))) {
$twig->display('error_box.html.twig', ['errors' => ['Account Two-Factor E-Mail Authentication disabled.']]);
return;
}
if ($twoFactorAuth->isActive()) {
$errors[] = 'Two-factor authentication is already enabled on your account.';
$twig->display('error_box.html.twig', ['errors' => $errors]);
return;
}
if (!$twoFactorAuth->hasRecentEmailCode(15 * 60)) {
$twoFactorAuth->resendEmailCode();
}
if (isset($_POST['save'])) {
if (!empty($code)) {
$twoFactorAuth->setAuthGateway(TwoFactorAuth::TYPE_EMAIL);
if ($twoFactorAuth->getAuthGateway()->verifyCode($code)) {
$serverName = configLua('serverName');
$twoFactorAuth->enable(TwoFactorAuth::TYPE_EMAIL);
$twoFactorAuth->deleteOldCodes();
$twig->display('success.html.twig', [
'title' => 'Email Code Authentication Activated',
'description' => sprintf('You have successfully activated <b>email code authentication</b> for your account. This means an <b>email code</b> will be sent to the email address assigned to your account whenever you try to log in to the %s client or the %s website. In order to log in, you will need to enter the <b>most recent email code</b> you have received.', $serverName, $serverName)
]);
return;
}
else {
$errors[] = 'Invalid email code!';
}
}
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', ['errors' => $errors]);
}
$twig->display('account/2fa/email/enable.html.twig', ['wrongCode' => count($errors) > 0]);

View File

@@ -1,32 +0,0 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';
if ((!setting('core.mail_enabled'))) {
$twig->display('error_box.html.twig', ['errors' => ['Account Two-Factor E-Mail Authentication disabled.']]);
return;
}
if (!$account_logged->isLoaded()) {
error('Account not found!');
return;
}
if ($twoFactorAuth->isActive($twoFactorAuth::TYPE_APP)) {
error('You have to disable the app auth first!');
return;
}
if ($twoFactorAuth->hasRecentEmailCode(30 * 60)) {
$errors = ['Sorry, one email per 30 minutes'];
}
else {
$twoFactorAuth->resendEmailCode();
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', ['errors' => $errors]);
}
$twig->display('account/2fa/email/enable.html.twig');

View File

@@ -17,10 +17,6 @@ if(!$logged)
if(!empty($errors))
$twig->display('error_box.html.twig', array('errors' => $errors));
if (defined('HIDE_LOGIN_BOX') && HIDE_LOGIN_BOX) {
return;
}
$twig->display('account.login.html.twig', array(
'redirect' => $_REQUEST['redirect'] ?? null,
'account' => USE_ACCOUNT_NAME ? 'Name' : 'Number',
@@ -34,11 +30,3 @@ if(!$logged)
else {
$show_form = true;
}
function generateRecoveryKey(): string
{
return generateRandomString(5, false, true, true) . '-' .
generateRandomString(5, false, true, true) . '-' .
generateRandomString(5, false, true, true) . '-' .
generateRandomString(5, false, true, true);
}

View File

@@ -10,7 +10,6 @@
*/
use MyAAC\RateLimit;
use MyAAC\TwoFactorAuth\TwoFactorAuth;
defined('MYAAC') or die('Direct access not allowed!');
@@ -53,18 +52,8 @@ if(!empty($login_account) && !empty($login_password))
$errors[] = 'Your account is not verified. Please verify your email address. If the message is not coming check the SPAM folder in your E-Mail client.<br/>' .
'You can resend the Email here: <a href="' . $link . '">' . $link . '</a>';
} else {
setSession('account', $account_logged->getId());
if (!$hooks->trigger(HOOK_ACCOUNT_LOGIN_PRE)) {
return;
}
$twoFactorAuth = TwoFactorAuth::getInstance($account_logged);
if (!$twoFactorAuth->process($login_account, $login_password, $remember_me, $_POST['auth-code'] ?? '')) {
return;
}
session_regenerate_id();
setSession('account', $account_logged->getId());
setSession('password', encrypt((USE_ACCOUNT_SALT ? $account_logged->getCustomField('salt') : '') . $login_password));
if($remember_me) {
setSession('remember_me', true);

View File

@@ -8,9 +8,6 @@
* @copyright 2019 MyAAC
* @link https://my-aac.org
*/
use MyAAC\TwoFactorAuth\TwoFactorAuth;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Account Management';
@@ -119,8 +116,6 @@ $twig->display('account.management.html.twig', array(
'account_registered' => $account_registered,
'account_rlname' => $account_rlname,
'account_location' => $account_location,
'twoFactorViews' => TwoFactorAuth::getInstance($account_logged)->getAccountManageViews(),
'actions' => $actions,
'players' => $account_players,
'players' => $account_players
));

View File

@@ -37,7 +37,7 @@ else
if($points >= setting('core.account_generate_new_reckey_price'))
{
$show_form = false;
$new_rec_key = generateRecoveryKey();
$new_rec_key = generateRandomString(10, false, true, true);
$mailBody = $twig->render('mail.account.register.html.twig', array(
'recovery_key' => $new_rec_key

View File

@@ -27,7 +27,7 @@ if(isset($_POST['registeraccountsave']) && $_POST['registeraccountsave'] == "1")
if($reg_password == $account_logged->getPassword()) {
if(empty($old_key)) {
$show_form = false;
$new_rec_key = generateRecoveryKey();
$new_rec_key = generateRandomString(10, false, true, true);
$account_logged->setCustomField("key", $new_rec_key);
$account_logged->logAction('Generated recovery key.');

View File

@@ -13,7 +13,7 @@ use MyAAC\Cache\Cache;
use MyAAC\Models\Player;
use MyAAC\Models\PlayerDeath;
use MyAAC\Models\PlayerKillers;
use MyAAC\Server\XML\Vocations;
use MyAAC\Server\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Highscores';

View File

@@ -12,7 +12,7 @@
use MyAAC\Cache\Cache;
use MyAAC\Models\ServerConfig;
use MyAAC\Models\ServerRecord;
use MyAAC\Server\XML\Vocations;
use MyAAC\Server\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Who is online?';
@@ -87,13 +87,16 @@ $cached = Cache::remember("online_$order", setting('core.online_cache_ttl') * 60
'name' => getPlayerLink($player['name']),
'player' => $player,
'level' => $player['level'],
'vocation' => $configVocations[$player['vocation']],
'vocation' => $configVocations[$player['vocation']] ?? 'Unknown',
'skull' => $skull,
'country_image' => getFlagImage($player['country']),
'outfit' => setting('core.outfit_images_url') . '?id=' . $player['looktype'] . ($outfit_addons ? '&addons=' . $player['lookaddons'] : '') . '&head=' . $player['lookhead'] . '&body=' . $player['lookbody'] . '&legs=' . $player['looklegs'] . '&feet=' . $player['lookfeet'],
);
$vocations[Vocations::getOriginal($player['vocation'])]++;
$originalId = Vocations::getOriginal($player['vocation']);
if ($originalId) {
$vocations[$originalId]++;
}
}
$record = '';

View File

@@ -40,7 +40,7 @@ class DataLoader
{
self::$startTime = microtime(true);
if(Items::loadFromXML()) {
if(Items::load()) {
success(self::$locale['step_database_loaded_items'] . self::getLoadedTime());
}
else {

View File

@@ -16,57 +16,37 @@ use MyAAC\Models\Spell;
class Items
{
private static $error = '';
public static $items;
public static array $items = [];
public static function loadFromXML($show = false)
{
$file_path = config('data_path') . 'items/items.xml';
if (!file_exists($file_path)) {
self::$error = 'Cannot load file ' . $file_path;
return false;
}
private static string $error = '';
$xml = new \DOMDocument;
$xml->load($file_path);
$items = array();
foreach ($xml->getElementsByTagName('item') as $item) {
if ($item->getAttribute('fromid')) {
for ($id = $item->getAttribute('fromid'); $id <= $item->getAttribute('toid'); $id++) {
$tmp = self::parseNode($id, $item, $show);
$items[$tmp['id']] = $tmp['content'];
}
} else {
$tmp = self::parseNode($item->getAttribute('id'), $item, $show);
$items[$tmp['id']] = $tmp['content'];
}
}
$cache_php = new CachePHP(config('cache_prefix'), CACHE . 'persistent/');
$cache_php->set('items', $items, 5 * 365 * 24 * 60 * 60);
return true;
}
public static function parseNode($id, $node, $show = false) {
$name = $node->getAttribute('name');
$article = $node->getAttribute('article');
$plural = $node->getAttribute('plural');
$attributes = array();
foreach($node->getElementsByTagName('attribute') as $attr) {
$attributes[strtolower($attr->getAttribute('key'))] = $attr->getAttribute('value');
}
return array('id' => $id, 'content' => array('article' => $article, 'name' => $name, 'plural' => $plural, 'attributes' => $attributes));
}
public static function getError() {
public static function getError(): string {
return self::$error;
}
public static function load() {
if(self::$items) {
public static function load(): bool {
$file_path = config('data_path') . 'items/items.toml';
if (file_exists($file_path)) {
$items = new Server\TOML\Items();
}
elseif (file_exists(config('data_path') . 'items/items.xml')) {
$items = new Server\XML\Items();
}
else {
self::$error = 'Cannot load items.xml or items.toml file. Files not found.';
return false;
}
if (!$items->load()) {
self::$error = $items->getError();
return false;
}
return true;
}
public static function init(): void {
if(count(self::$items) > 0) {
return;
}
@@ -75,7 +55,7 @@ class Items
}
public static function get($id) {
self::load();
self::init();
return self::$items[$id] ?? [];
}

View File

@@ -1,14 +0,0 @@
<?php
namespace MyAAC\Models;
use Illuminate\Database\Eloquent\Model;
class AccountEMailCode extends Model {
protected $table = TABLE_PREFIX . 'account_email_codes';
public $timestamps = false;
protected $fillable = ['account_id', 'code', 'created_at'];
}

View File

@@ -39,7 +39,7 @@ class Monsters {
}
$items = array();
Items::load();
Items::init();
foreach((array)Items::$items as $id => $item) {
$items[$item['name']] = $id;
}

View File

@@ -0,0 +1,29 @@
<?php
namespace MyAAC\Server;
use MyAAC\Cache\Cache;
class Groups
{
private static array $groups = [];
public function __construct() {
self::$groups = Cache::remember('groups', 10 * 60, function () {
if (file_exists(config('server_path') . 'config/groups.toml')) {
$groups = new TOML\Groups();
}
else {
$groups = new XML\Groups();
}
$groups->load();
return $groups->getGroups();
});
}
public static function getGroups(): array {
return self::$groups;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace MyAAC\Server\TOML;
use Devium\Toml\Toml;
class Groups
{
private array $groups = [];
public function load(): void
{
$file = config('server_path') . 'config/groups.toml';
if(!@file_exists($file)) {
error('Error: Cannot load groups.toml. More info in system/logs/error.log file.');
log_append('error.log', "[OTS_Groups_List.php] Fatal error: Cannot load groups.toml ($file). It doesn't exist.");
return;
}
$toml = file_get_contents($file);
$groups = Toml::decode($toml, asArray: true);
foreach ($groups as $group)
{
$this->groups[$group['id']] = [
'id' => $group['id'],
'name' => $group['name'],
'access' => $group['access'],
];
}
}
public function getGroups(): array {
return $this->groups;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace MyAAC\Server\TOML;
use MyAAC\Cache\PHP as CachePHP;
class Items
{
private string $error;
public function getError(): string
{
return $this->error;
}
public function load(): bool
{
$file_path = config('data_path') . 'items/items.toml';
if (!file_exists($file_path)) {
$this->error = 'Cannot load file ' . $file_path;
return false;
}
//$toml = file_get_contents($file_path);
//$items = \Devium\Toml\Toml::decode($toml, asArray: false);
$itemsParser = new ItemsParser();
$itemsParsed = $itemsParser->parse($file_path);
$items = [];
foreach ($itemsParsed as $item) {
$attributes = array_filter($item, function ($key) {
return !in_array($key, ['id', 'article', 'name', 'plural']);
}, ARRAY_FILTER_USE_KEY);
$id = $item['id'] ?? null;
if ($id === null) {
continue;
}
$article = $item['article'] ?? '';
$name = $item['name'] ?? '';
$plural = $item['plural'] ?? '';
$items[$id] = [
'article' => $article,
'name' => $name,
'plural' => $plural,
'attributes' => $attributes,
];
}
$cache_php = new CachePHP(config('cache_prefix'), CACHE . 'persistent/');
$cache_php->set('items', $items, 5 * 365 * 24 * 60 * 60);
return true;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace MyAAC\Server\TOML;
class ItemsParser
{
public function parse(string $path): array
{
$ret = [];
$i = 0;
$handle = fopen($path, "r");
if ($handle) {
$parse = '';
while (($line = fgets($handle)) !== false) {
if (str_contains($line, '[[items]]') && $i++ != 0) {
//global $whoopsHandler;
//$whoopsHandler->addDataTable('ini', [$parse]);
$ret[] = parse_ini_string($parse);
$parse = '';
continue;
}
// skip lines like this
// field = {type = "fire", initdamage = 20, ticks = 10000, count = 7, damage = 10}
// as it cannot be parsed by parse_ini_string
if (str_contains($line, 'field =')) {
continue;
}
$parse .= $line;
}
$ret[] = parse_ini_string($parse);
fclose($handle);
}
return $ret;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace MyAAC\Server\TOML;
use Devium\Toml\Toml;
class Vocations
{
private array $vocations = [];
private array $vocationsFrom = [];
public function load(): void
{
$tomlVocations = glob(config('data_path') . 'vocations/*.toml');
if (count($tomlVocations) <= 0) {
throw new \RuntimeException('ERROR: Cannot load any .toml vocation from the data/vocations folder.');
}
foreach ($tomlVocations as $file) {
$toml = file_get_contents($file);
$vocations = Toml::decode($toml, asArray: true);
foreach ($vocations as $vocationArray) {
$id = $vocationArray['id'];
$this->vocations[$id] = $vocationArray['name'];
$this->vocationsFrom[$id] = $vocationArray['promotedfrom'];
}
}
ksort($this->vocations, SORT_NUMERIC);
ksort($this->vocationsFrom, SORT_NUMERIC);
}
public function get(): array {
return $this->vocations;
}
public function getFrom(): array {
return $this->vocationsFrom;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace MyAAC\Server;
use MyAAC\Cache\Cache;
class Vocations
{
private static array $vocations = [];
private static array $vocationsFrom = [];
public function __construct() {
$cached = Cache::remember('vocations', 10 * 60, function () {
$tomlVocations = glob(config('data_path') . 'vocations/*.toml');
if (count($tomlVocations) > 0) {
$vocations = new TOML\Vocations();
}
else {
$vocations = new XML\Vocations();
}
$vocations->load();
$from = $vocations->getFrom();
$amount = 0;
foreach ($from as $vocId => $fromVocation) {
if ($vocId != 0 && $vocId == $fromVocation) {
$amount++;
}
}
return ['vocations' => $vocations->get(), 'vocationsFrom' => $from, 'amount' => $amount];
});
self::$vocations = $cached['vocations'];
self::$vocationsFrom = $cached['vocationsFrom'];
config(['vocations', self::$vocations]);
config(['vocations_amount', $cached['amount']]);
}
public static function get(): array {
return self::$vocations;
}
public static function getFrom(): array {
return self::$vocationsFrom;
}
public static function getPromoted(int $id): ?int {
foreach (self::$vocationsFrom as $vocId => $fromVocation) {
if ($id == $fromVocation && $vocId != $id) {
return $vocId;
}
}
return null;
}
public static function getOriginal(int $id): ?int {
if (!isset(self::$vocationsFrom[$id])) {
return null;
}
while ($tmpId = self::$vocationsFrom[$id]) {
if ($tmpId == $id) {
break;
}
$id = $tmpId;
}
return $id;
}
public static function getBase($includingRook = true): array {
$vocations = [];
foreach (self::$vocationsFrom as $vocId => $fromVoc) {
if ($vocId == $fromVoc && ($vocId != 0 || $includingRook)) {
$vocations[] = $vocId;
}
}
return $vocations;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace MyAAC\Server\XML;
class Groups
{
private array $groups = [];
public function load(): void
{
$file = config('data_path') . 'XML/groups.xml';
if(!@file_exists($file)) {
error('Error: Cannot load groups.xml. More info in system/logs/error.log file.');
log_append('error.log', "[OTS_Groups_List.php] Fatal error: Cannot load groups.xml ($file). It doesn't exist.");
return;
}
$groups = new \DOMDocument();
if(!@$groups->load($file)) {
error('Error: Cannot load groups.xml. More info in system/logs/error.log file.');
log_append('error.log', '[OTS_Groups_List.php] Fatal error: Cannot load groups.xml (' . $file . '). Error: ' . print_r(error_get_last(), true));
return;
}
// loads groups
foreach( $groups->getElementsByTagName('group') as $group)
{
$this->groups[$group->getAttribute('id')] = [
'id' => $group->getAttribute('id'),
'name' => $group->getAttribute('name'),
'access' => $group->getAttribute('access')
];
}
}
public function getGroups(): array {
return $this->groups;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace MyAAC\Server\XML;
use MyAAC\Cache\PHP as CachePHP;
class Items
{
private string $error;
public function getError(): string {
return $this->error;
}
public function load(): bool
{
$file_path = config('data_path') . 'items/items.xml';
if (!file_exists($file_path)) {
$this->error = 'Cannot load file ' . $file_path;
return false;
}
$xml = new \DOMDocument;
$xml->load($file_path);
$items = [];
foreach ($xml->getElementsByTagName('item') as $item) {
if ($item->getAttribute('fromid')) {
for ($id = $item->getAttribute('fromid'); $id <= $item->getAttribute('toid'); $id++) {
$tmp = $this->parseNode($id, $item);
$items[$tmp['id']] = $tmp['content'];
}
} else {
$tmp = $this->parseNode($item->getAttribute('id'), $item);
$items[$tmp['id']] = $tmp['content'];
}
}
$cache_php = new CachePHP(config('cache_prefix'), CACHE . 'persistent/');
$cache_php->set('items', $items, 5 * 365 * 24 * 60 * 60);
return true;
}
public function parseNode($id, $node): array
{
$name = $node->getAttribute('name');
$article = $node->getAttribute('article');
$plural = $node->getAttribute('plural');
$attributes = array();
foreach($node->getElementsByTagName('attribute') as $attr) {
$attributes[strtolower($attr->getAttribute('key'))] = $attr->getAttribute('value');
}
return ['id' => $id, 'content' => ['article' => $article, 'name' => $name, 'plural' => $plural, 'attributes' => $attributes]];
}
}

View File

@@ -2,35 +2,10 @@
namespace MyAAC\Server\XML;
use MyAAC\Cache\Cache;
class Vocations
{
private static array $vocations;
private static array $vocationsFrom;
public function __construct()
{
$cached = Cache::remember('vocations', 10 * 60, function () {
$this->load();
$from = $this->getFrom();
$amount = 0;
foreach ($from as $vocId => $fromVocation) {
if ($vocId != 0 && $vocId == $fromVocation) {
$amount++;
}
}
return ['vocations' => $this->get(), 'vocationsFrom' => $from, 'amount' => $amount];
});
self::$vocations = $cached['vocations'];
self::$vocationsFrom = $cached['vocationsFrom'];
config(['vocations', self::$vocations]);
config(['vocations_amount', $cached['amount']]);
}
private array $vocations = [];
private array $vocationsFrom = [];
public function load(): void
{
@@ -51,51 +26,26 @@ class Vocations
foreach($vocationsXML->getElementsByTagName('vocation') as $vocation) {
$id = $vocation->getAttribute('id');
self::$vocations[$id] = $vocation->getAttribute('name');
$this->vocations[$id] = $vocation->getAttribute('name');
$fromVocation = (int) $vocation->getAttribute('fromvoc');
self::$vocationsFrom[$id] = $fromVocation;
$this->vocationsFrom[$id] = $fromVocation;
}
}
public static function get(): array {
return self::$vocations;
public function get(): array {
return $this->vocations;
}
public static function getFrom(): array {
return self::$vocationsFrom;
}
public static function getPromoted(int $id): ?int {
foreach (self::$vocationsFrom as $vocId => $fromVocation) {
if ($id == $fromVocation && $vocId != $id) {
return $vocId;
}
}
return null;
}
public static function getOriginal(int $id): ?int {
while ($tmpId = self::$vocationsFrom[$id]) {
if ($tmpId == $id) {
break;
}
$id = $tmpId;
}
return $id;
public function getFrom(): array {
return $this->vocationsFrom;
}
public static function getBase($includingRook = true): array {
$vocations = [];
foreach (self::$vocationsFrom as $vocId => $fromVoc) {
if ($vocId == $fromVoc && ($vocId != 0 || $includingRook)) {
$vocations[] = $vocId;
}
}
return \MyAAC\Server\Vocations::getBase($includingRook);
}
return $vocations;
public static function getOriginal(int $id): ?int {
return \MyAAC\Server\Vocations::getOriginal($id);
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace MyAAC\TwoFactorAuth\Gateway;
use MyAAC\TwoFactorAuth\Interface\AuthGatewayInterface;
use OTPHP\TOTP;
class AppAuthGateway extends BaseAuthGateway implements AuthGatewayInterface
{
public function verifyCode(string $code): bool
{
$otp = TOTP::createFromSecret($this->account->getCustomField('secret'));
$otp->setLabel($this->account->getEmail());
$otp->setIssuer(configLua('serverName'));
return $otp->verify($code);
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace MyAAC\TwoFactorAuth\Gateway;
class BaseAuthGateway
{
protected \OTS_Account $account;
public function __construct(\OTS_Account $account) {
$this->account = $account;
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace MyAAC\TwoFactorAuth\Gateway;
use MyAAC\Models\AccountEMailCode;
use MyAAC\TwoFactorAuth\Interface\AuthGatewayInterface;
use MyAAC\TwoFactorAuth\TwoFactorAuth;
class EmailAuthGateway extends BaseAuthGateway implements AuthGatewayInterface
{
public function verifyCode(string $code): bool
{
return AccountEMailCode::where('account_id', '=', $this->account->getId())->where('code', $code)->where('created_at', '>', time() - TwoFactorAuth::EMAIL_CODE_VALID_UNTIL)->first() !== null;
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace MyAAC\TwoFactorAuth\Interface;
interface AuthGatewayInterface
{
public function __construct(\OTS_Account $account);
public function verifyCode(string $code): bool;
}

View File

@@ -1,270 +0,0 @@
<?php
namespace MyAAC\TwoFactorAuth;
use MyAAC\Models\AccountEMailCode;
use MyAAC\TwoFactorAuth\Gateway\AppAuthGateway;
use MyAAC\TwoFactorAuth\Gateway\EmailAuthGateway;
use OTPHP\TOTP;
class TwoFactorAuth
{
const TYPE_NONE = 0;
const TYPE_EMAIL = 1;
const TYPE_APP = 2;
// maybe later
//const TYPE_SMS = 3;
const EMAIL_CODE_VALID_UNTIL = 24 * 60 * 60;
private static self $instance;
private \OTS_Account $account;
private int $authType;
private EmailAuthGateway|AppAuthGateway $authGateway;
public function __construct(\OTS_Account|int $account) {
if (is_int($account)) {
$this->account = new \OTS_Account();
$this->account->load($account);
}
else {
$this->account = $account;
}
$this->authType = (int)$this->account->getCustomField('2fa_type');
$this->setAuthGateway($this->authType);
}
public static function getInstance($account = null): self
{
if (!isset(self::$instance)) {
self::$instance = new self($account);
}
return self::$instance;
}
public function process($login_account, $login_password, $remember_me, $code): bool
{
global $twig;
if (!$this->isActive()) {
return true;
}
$view = 'app';
if ($this->authType == self::TYPE_EMAIL) {
$view = 'email';#
}
if (empty($code)) {
if ($this->authType == self::TYPE_EMAIL) {
if (!$this->hasRecentEmailCode(15 * 60)) {
$this->resendEmailCode();
}
}
define('HIDE_LOGIN_BOX', true);
$twig->display("account/2fa/$view/login.html.twig", [
'account_login' => $login_account,
'password_login' => $login_password,
'remember_me' => $remember_me,
]);
return false;
}
if ($this->getAuthGateway()->verifyCode($code)) {
if ($this->authType === self::TYPE_EMAIL) {
$this->deleteOldCodes();
}
return true;
}
if (setting('core.mail_enabled')) {
$mailBody = $twig->render('mail.account.2fa.email-code.wrong-attempt.html.twig');
if (!_mail($this->account->getEMail(), configLua('serverName') . ' - Failed Two-Factor Authentication Attempt', $mailBody)) {
error('An error occurred while sending email. For Admin: More info can be found in system/logs/mailer-error.log');
}
}
define('HIDE_LOGIN_BOX', true);
if ($this->authType == self::TYPE_APP) {
$errors[] = 'The token is invalid!';
}
else {
$errors[] = 'Invalid E-Mail code!';
}
$twig->display('error_box.html.twig', ['errors' => $errors]);
$twig->display("account/2fa/$view/login.html.twig",
[
'account_login' => $login_account,
'password_login' => $login_password,
'remember_me' => $remember_me,
'wrongCode' => true,
]);
return false;
}
public function processClientLogin($code, string &$error, &$errorCode): bool
{
if (!$this->isActive()) {
return true;
}
if ($this->authType == self::TYPE_EMAIL) {
$errorCode = 8;
}
if ($code === false) {
$error = 'Submit a valid two-factor authentication token.';
if ($this->authType == self::TYPE_EMAIL) {
if (!$this->hasRecentEmailCode(15 * 60)) {
$this->resendEmailCode();
}
}
return false;
}
if (!$this->getAuthGateway()->verifyCode($code)) {
$error = 'Two-factor authentication failed, token is wrong.';
return false;
}
if ($this->authType === self::TYPE_EMAIL) {
$this->deleteOldCodes();
}
return true;
}
public function setAuthGateway(int $authType): void
{
if ($authType === self::TYPE_EMAIL) {
$this->authGateway = new EmailAuthGateway($this->account);
}
else if ($authType === self::TYPE_APP) {
$this->authGateway = new AppAuthGateway($this->account);
}
}
public function getAccountManageViews(): array
{
if ($this->authType == self::TYPE_EMAIL) {
$twoFactorView = 'account/2fa/main.protected.html.twig';
$twoFactorView2 = 'account/2fa/email/manage.connected.html.twig';
}
elseif ($this->authType == self::TYPE_APP) {
$twoFactorView = 'account/2fa/app/manage.connected.html.twig';
$twoFactorView2 = 'account/2fa/main.protected.html.twig';
}
else {
$twoFactorView = 'account/2fa/app/manage.enable.html.twig';
$twoFactorView2 = 'account/2fa/email/manage.enable.html.twig';
}
return [$twoFactorView, $twoFactorView2];
}
public function enable(int $type): void {
$this->account->setCustomField('2fa_type', $type);
}
public function disable(): void
{
global $db;
$this->account->setCustomField('2fa_type', self::TYPE_NONE);
if ($db->hasColumn('accounts', 'secret')) {
$this->account->setCustomField('secret', null);
}
$this->account->setCustomField('2fa_secret', '');
}
public function isActive(?int $authType = null): bool {
if ($authType !== null) {
return $this->authType === $authType;
}
return $this->authType != self::TYPE_NONE;
}
public function getAuthType(): int {
return $this->authType;
}
public function getAuthGateway(): AppAuthGateway|EmailAuthGateway {
return $this->authGateway;
}
public function hasRecentEmailCode($since = self::EMAIL_CODE_VALID_UNTIL): bool {
return AccountEMailCode::where('account_id', '=', $this->account->getId())->where('created_at', '>', time() - $since)->first() !== null;
}
public function deleteOldCodes(): void {
AccountEMailCode::where('account_id', '=', $this->account->getId())->delete();
}
public function appInitTOTP(string $secret): TOTP
{
$otp = TOTP::createFromSecret($secret);
$otp->setLabel($this->account->getEmail());
$otp->setIssuer(configLua('serverName'));
return $otp;
}
public function appDisplayEnable(string $secret, ?TOTP $otp = null, array $errors = []): void
{
global $twig;
if ($otp === null) {
$otp = $this->appInitTOTP($secret);
}
$grCodeUri = $otp->getQrCodeUri(
'https://api.qrserver.com/v1/create-qr-code/?data=[DATA]&size=200x200&ecc=M',
'[DATA]'
);
$twig->display('account/2fa/app/enable.html.twig', [
'grCodeUri' => $grCodeUri,
'secret' => $secret,
'errors' => $errors,
]);
}
public function resendEmailCode(): void
{
global $twig;
$newCode = generateRandomString(6, true, false, true);
AccountEMailCode::create([
'account_id' => $this->account->getId(),
'code' => $newCode,
'created_at' => time(),
]);
$mailBody = $twig->render('mail.account.2fa.email-code.html.twig', [
'code' => $newCode,
]);
if (!_mail($this->account->getEMail(), configLua('serverName') . ' - Requested Authentication Email Code', $mailBody)) {
error('An error occurred while sending email. For Admin: More info can be found in system/logs/mailer-error.log');
}
}
}

View File

@@ -69,7 +69,6 @@ define('HOOK_ACCOUNT_LOGIN_AFTER_PASSWORD', ++$i);
define('HOOK_ACCOUNT_LOGIN_AFTER_REMEMBER_ME', ++$i);
define('HOOK_ACCOUNT_LOGIN_AFTER_PAGE', ++$i);
define('HOOK_ACCOUNT_LOGIN_POST', ++$i);
define('HOOK_ACCOUNT_LOGIN_PRE', ++$i);
define('HOOK_ACCOUNT_LOST_CHECK_CODE_FINISH_AFTER_PASSWORD', ++$i);
define('HOOK_ACCOUNT_LOST_CHECK_CODE_FINISH_AFTER_PASSWORD_REPEAT', ++$i);
define('HOOK_ACCOUNT_LOST_EMAIL_SET_NEW_PASSWORD_POST', ++$i);

View File

@@ -147,9 +147,6 @@
{% include('buttons.base.html.twig') %}
</form>
<br/>
{{ include('account/2fa/main.html.twig') }}
{{ hook('HOOK_ACCOUNT_MANAGE_BEFORE_ACCOUNT_LOGS') }}
<a name="Account+Logs" ></a>
<h2>Account Logs</h2>

View File

@@ -1,76 +0,0 @@
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableContentContainer ">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<ol>
<li>Open an authenticator app of your choice (e.g. <a
target="_blank"
href="https://support.google.com/accounts/answer/1066447"
rel="noopener noreferrer">Google Authenticator</a>, <a
target="_blank" href="https://www.authy.com/users"
rel="noopener noreferrer">Authy</a>). In the app you
will be asked either to enter a key manually:<br><b>{{ secret }}</b><br>or
to scan the barcode below:<br>
<img alt="QR code" style="margin-top: 15px; margin-bottom: 15px;"
src="{{ grCodeUri }}">
</li>
<li><label for="totp">Enter the verification code you have received from the used
authenticator app:</label><br>
<div style="margin-top: 15px; margin-bottom: 15px;">
<input form="form" id="auth-code" name="auth-code" maxlength="6" autocomplete="off">
{% if errors|length > 0 %}
<br/>
<div class="FormFieldError">{{ errors[0] }}</div>
{% endif %}
</div>
</li>
<li>Click on "Continue" to connect the authenticator app to your
Tibia account.
</li>
</ol>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<br>
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="form" method="post" action="{{ getLink('account/2fa/app/enable') }}">
<input type="hidden" name="action" value="link">
{{ csrf() }}
{% set button_color = 'green' %}
{% set button_name = 'Continue' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_color = 'blue' %}
{% set button_name = 'Cancel' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -1,101 +0,0 @@
{% set title = 'Warning' %}
{% set background = config('darkborder') %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<span class="red"><b>Please read this warning carefully as it contains important security information! If you skip this message, you might lose your {{ config.lua.serverName }} account!</b></span><br><br>
<p>Before you connect your account with an authenticator app, you will be asked to
enter your recovery key. If you do not have a valid recovery key, you need to
order a new one before you can connect your account with an authenticator.</p>
<p>Why?<br>The recovery key is the only way to unlink the authenticator app from
your {{ config.lua.serverName }} account in various cases, among others, if:</p>
<ul style="list-style-type:square">
<li>you lose your device (mobile phone, tablet, etc.) with the authenticator
app
</li>
<li>the device with the authenticator app does not work anymore</li>
<li>the device with the authenticator app gets stolen</li>
<li>you delete the authenticator app from your device and reinstall it</li>
<li>your device is reset for some reason</li>
</ul>
<p></p>
<p>Please note that the authenticator app data is not saved on your device's account
(e.g. Google or iTunes sync) even if you have app data backup&amp;synchronisation
activated in the settings of your device!</p>
<p>In all these scenarios, the recovery key is the only way to get access to your
{{ config.lua.serverName }} account. Note that not even customer support will be able to help you in
these cases if you do not have a valid recovery key.<br>For this reason, make
sure to store your recovery key always in a safe place!</p><br>Do you have a
valid recovery key and would like to request the email with the confirmation key to
start connecting your {{ config.lua.serverName }} account to an authenticator app?<br><br><b>Enter your
recovery key:</b><br/>
<div style="margin-top: 15px; margin-bottom: 15px;">
{% if newRecoveryKeyFormat %}
<input form="form" class="UpperCaseInput" name="key1" value="" size="5" maxlength="5" autocomplete="off"> -
<input form="form" class="UpperCaseInput" name="key2" value="" size="5" maxlength="5" autocomplete="off"> - <input form="form" class="UpperCaseInput" name="key3" value="" size="5" maxlength="5" autocomplete="off"> -
<input form="form" class="UpperCaseInput" name="key4" value="" size="5" maxlength="5" autocomplete="off">
{% else %}
<input form="form" class="UpperCaseInput" name="key" value="" autocomplete="off">
{% endif %}
{% if errors|length > 0 %}
<br/>
<div class="FormFieldError">{{ errors[0] }}</div>
{% endif %}
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
<br>
<table style="width:100%;">
<tbody>
<tr align="center">
<td>
<form id="form" action="{{ getLink('account/2fa/app/enable') }}" method="post" style="padding:0;margin:0;">
<input type="hidden" name="action" value="request" />
{{ csrf() }}
{% set button_name = 'Request' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/register') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Order Recovery Key' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Cancel Request' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>
<style>
.UpperCaseInput {
text-transform: uppercase;
}
</style>

View File

@@ -1,64 +0,0 @@
{% set title = 'Enter Authenticator App Token' %}
{% set background = config('darkborder') %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableContentContainer ">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>Enter the verification code generated by the app:<br>
<div style="margin-top: 15px; margin-bottom: 15px;">
<div class="LabelV200" style="float:left;">Authenticator App Token:</div>
<input form="form" id="auth-code" name="auth-code" maxlength="6" autocomplete="off" required autofocus></div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
<br/>
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="form" action="{{ getLink('account/manage') }}" method="post">
{{ csrf() }}
<input type="hidden" name="account_login" value="{{ account_login ?? '' }}" />
<input type="hidden" name="password_login" value="{{ password_login ?? '' }}" />
{% if remember_me %}
<input type="hidden" name="remember_me" value="true" />
{% endif %}
<input type="hidden" name="step" value="verify">
{% set button_color = 'green' %}
{% set button_name = 'Continue' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post"
style="padding:0;margin:0;">
{{ csrf() }}
{% set button_color = 'blue' %}
{% set button_name = 'Cancel' %}
{{ include('buttons.base.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -1,25 +0,0 @@
<tr>
<td>
<div class="TableContentContainer ">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<div style="float: right; width: 135px;">
<form action="{{ getLink('account/2fa/app/disable') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Unlink' %}
{{ include('buttons.base.html.twig') }}
</form>
</div>
<b>Your Tibia account is <span style="color: green">connected</span> to an authenticator app.</b>
<p>If you do not want to use an authenticator app any longer, you can "Unlink" the authenticator
App. Note, however, an authenticator app is an important security feature which helps to
prevent any unauthorized access to your Tibia account.</p></td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>

View File

@@ -1,36 +0,0 @@
<tr>
<td>
<div class="TableShadowContainerRightTop">
<div class="TableShadowRightTop" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rt.gif);"></div>
</div>
<div class="TableContentAndRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rm.gif);">
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody><tr>
<td class="LabelV"><b>Connect your {{ config.lua.serverName }} account to an authenticator app!</b>
<div style="float: right; font-size: 1px;">
<form action="{{ getLink('account/2fa/app/enable') }}" method="post" style="margin: 0; padding: 0;">
{{ csrf() }}
{% set button_name = 'Request' %}
{% include('buttons.base.html.twig') %}
</form>
</div>
</td>
</tr>
<tr>
<td>
<p>As a first step to connect an <b>authenticator app</b> to your account, click on "Request"! An email with a confirmation key will be sent to the email address assigned to your account.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="TableShadowContainer">
<div class="TableBottomShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bm.gif);">
<div class="TableBottomLeftShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bl.gif);"></div>
<div class="TableBottomRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-br.gif);"></div>
</div>
</div>
</td>
</tr>

View File

@@ -1,108 +0,0 @@
{% set title = 'Deactivate Email Code Authentication' %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableContentContainer ">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>To disable <b>two-factor email code authentication</b> for your account, enter the
received <b>email code</b> below. Note, however, that <b>email code authentication</b>
is an important security feature which helps to prevent any unauthorized access to your
Tibia account.
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<div style="float: right;">
<form
action="{{ getLink('account/2fa/email/resend-code') }}"
method="post"
style="padding:0;margin:0;"
>
{{ csrf() }}
{% set button_name = 'Resend Email Code' %}
{{ include('buttons.base.html.twig') }}
</form>
</div>
An <b>email code</b> has already been sent to the email address assigned to your
account.
Please check your email account's spam/junk filter and make sure that your mailbox is
not
full.<br>In case you need a new email code, you can request one by clicking on "Resend
Email
Code".
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>To complete the deactivation of <b>email code authentication</b>, please enter the <b>email
code</b> you received at the email address assigned to your account.
<div style="margin-top: 15px; margin-bottom: 15px;">
<div class="LabelV150 {{ wrongCode ? 'red' : '' }}" style="float:left;"><label
for="email-code">Email Code:</label></div>
<input form="form" id="auth-code" name="email-code" maxlength="15"
autocomplete="off">
{% if wrongCode %}
<br/>
<div class="LabelV150" style="float:left;">&nbsp; </div>
<div class="FormFieldError">Invalid email code!</div>
{% endif %}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="form" method="post" action="{{ getLink('account/2fa/email/disable') }}">
{{ csrf() }}
<input type="hidden" name="save" value="1">
{% set button_name = 'Continue' %}
{% set button_color = 'green' %}
{{ include('buttons.submit.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_color = 'blue' %}
{{ include('buttons.back.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -1,108 +0,0 @@
{% set title = 'Activate Email Code Authentication' %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>Enter the email code below to enable <b>two-factor email code authentication</b>. Note
that this code is only valid for 24 hours.<br><br>
<div class="AttentionSign"><img src="{{ template_path }}/images/global/content/attentionsign.gif"></div>
<b>Note:</b> Once you have email code authentication enabled, an <b>email code</b> will be
sent to the email address assigned to your account whenever you try to log in to the Tibia
client or the {{ config.lua.serverName }} website. In order to log in, you will need to enter the <b>most recent
email code</b> you have received.
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<div style="float: right;">
<form action="{{ getLink('account/2fa/email/resend-code') }}"
method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% if account_logged is defined %}
<input type="hidden" name="account_logged" value="{{ account_logged.getId() }}">
{% endif %}
{% set button_name = 'Resend Email Code' %}
{% include('buttons.base.html.twig') %}
</form>
</div>
An <b>email code</b> has already been sent to the email address assigned to your account.
Please check your email account's spam/junk filter and make sure that your mailbox is not
full.<br>In case you need a new email code, you can request one by clicking on "Resend Email
Code".
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>To complete the activation of email code authentication for your Tibia account, please enter
the email code you received at the email address assigned to your account.
<div style="margin-top: 15px; margin-bottom: 15px;">
<div class="LabelV150 {{ wrongCode ? 'red' : '' }}" style="float:left;">Email Code:</div>
<input form="form" name="auth-code" maxlength="6" autocomplete="off">
{% if wrongCode %}
<br/>
<div class="LabelV150" style="float:left;">&nbsp; </div>
<div class="FormFieldError">Invalid email code!</div>
{% endif %}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
<br/>
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="form" action="{{ getLink('account/2fa/email/enable') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
<input type="hidden" name="save" value="1">
{% set button_color = 'green' %}
{{ include('buttons.submit.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_color = 'blue' %}
{{ include('buttons.back.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -1,91 +0,0 @@
{% set title = 'Enter Email Code' %}
{% set content %}
<table style="width:100%;">
<tbody>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<div style="float: right;">
<form
action="{{ getLink('account/2fa/email/resend-code') }}"
method="post"
style="padding:0;margin:0;"
>
{{ csrf() }}
{% set button_name = 'Resend E-Mail Code' %}
{{ include('buttons.base.html.twig') }}
</form>
</div>
An <b>E-Mail code</b> has already been sent to the E-Mail address assigned to your account.
Please check your E-Mail account's spam/junk filter and make sure that your mailbox is not
full.<br>In case you need a new E-Mail code, you can request one by clicking on "Resend E-Mail Code".
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<tr>
<td>
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td><b>E-Mail code authentication is enabled for your account.</b><br><br>Please enter the <b>most
recent E-Mail code</b> you have received in order to log in.<br>
<div style="margin-top: 15px; margin-bottom: 15px;">
<div class="LabelV150 {{ wrongCode ? 'red' : '' }}" style="float:left;"><label for="email-code">E-Mail Code:</label></div>
<input form="form" id="auth-code" name="auth-code" maxlength="15" autocomplete="off" required autofocus>
{% if wrongCode %}
<br/>
<div class="LabelV150" style="float:left;">&nbsp; </div>
<div class="FormFieldError">Invalid E-Mail code!</div>
{% endif %}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{% endset %}
{% include 'tables.headline.html.twig' %}
<table style="width: 100%;">
<tbody>
<tr align="center" valign="top">
<td>
<form id="form" method="post" action="{{ getLink('account/manage') }}">
{{ csrf() }}
<input type="hidden" name="account_login" value="{{ account_login ?? '' }}" />
<input type="hidden" name="password_login" value="{{ password_login ?? '' }}" />
{% if remember_me %}
<input type="hidden" name="remember_me" value="true" />
{% endif %}
<input type="hidden" name="step" value="verify">
{% set button_name = 'Continue' %}
{% set button_color = 'green' %}
{{ include('buttons.submit.html.twig') }}
</form>
</td>
<td>
<form action="{{ getLink('account/manage') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_color = 'blue' %}
{{ include('buttons.back.html.twig') }}
</form>
</td>
</tr>
</tbody>
</table>

View File

@@ -1,26 +0,0 @@
<tr>
<td>
<div class="TableContentContainer ">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td>
<div style="float: right; width: 135px;">
<form action="{{ getLink('account/2fa/email/disable') }}" method="post" style="padding:0;margin:0;">
{{ csrf() }}
{% set button_name = 'Disable' %}
{{ include('buttons.base.html.twig') }}
</form>
</div>
<b>Two-Factor Email Code Authentication <span style="color: green">Enabled</span>!</b>
<p>To disable <b>email code authentication</b>, click on the "Disable" button.</p>
<!--p>You will have to confirm the deactivation by entering an <b>email code</b> which will be sent
to the email address assigned to your account.</p-->
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>

View File

@@ -1,37 +0,0 @@
<tr>
<td>
<div class="TableShadowContainerRightTop">
<div class="TableShadowRightTop" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rt.gif);"></div>
</div>
<div class="TableContentAndRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-rm.gif);">
<div class="TableContentContainer">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td class="LabelV"><b>Enable email code authentication for your account!</b>
<div style="float: right; font-size: 1px;">
<form action="{{ getLink('account/2fa/email/enable') }}" method="post" style="margin: 0; padding: 0;">
{{ csrf() }}
{% set button_name = 'Request' %}
{% include('buttons.base.html.twig') %}
</form>
</div>
</td>
</tr>
<tr>
<td>
<p>As a first step to enable <b>email code authentication</b> for your account, click on "Request"! An <b>email code</b> will be sent to the email address assigned to your account. You will be asked to enter this <b>email code</b> on the next page within 24 hours.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="TableShadowContainer">
<div class="TableBottomShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bm.gif);">
<div class="TableBottomLeftShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-bl.gif);"></div>
<div class="TableBottomRightShadow" style="background-image:url({{ template_path }}/images/global/content/table-shadow-br.gif);"></div>
</div>
</div>
</td>
</tr>

View File

@@ -1,12 +0,0 @@
{% set title = 'Two-Factor Authentication' %}
{% set content %}
<table style="width:100%;">
<tbody>
{{ include(twoFactorViews[0]) }}
{{ include(twoFactorViews[1]) }}
</tbody>
</table>
{% endset %}
{% include('tables.headline.html.twig') %}
<br/>

View File

@@ -1,22 +0,0 @@
{% if logged and account_logged.getCustomField('2fa_type') == 1 %}
{% set header = 'Two-Factor Email Code Authentication' %}
{% set text = 'Your account is currently protected by email code authentication. If you prefer to use a <strong>two-factor authentication app</strong>, you have to "Disable" email code authentication first.' %}
{% else %}
{% set header = 'Two-Factor App Code Authentication' %}
{% set text = 'Your account is currently protected by an authenticator app. If you prefer to use the <strong>two-factor email code authentication</strong>, you have to "Unlink" the authenticator app first.' %}
{% endif %}
<tr>
<td>
<div class="TableContentContainer ">
<table class="TableContent" width="100%" style="border:1px solid #faf0d7;">
<tbody>
<tr>
<td><b>{{ header|raw }}</b>
<p>{{ text|raw }}</p>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>

View File

@@ -1,9 +0,0 @@
Dear {{ config.lua.serverName}} player,
<br/><br/>
Your account is protected by email code authentication, and you requested a new email code:
<br/><br/>
<h1><strong>{{ code }}</strong></h1>
<br/>
Note that the code is only valid for 24 hours.
<br/><br/>
Kind Regards,

View File

@@ -1,5 +0,0 @@
Dear {{ config.lua.serverName}} player,<br/>
<br/>
A <strong>wrong two-factor authentication code</strong> was entered for your {{ config.lua.serverName}} account. If you simply mistyped the code, please try again.<br/>
<br/>
However, if this was <strong>not you</strong>, someone else may be trying to access your account. Since they already know your password, we strongly recommend that you <strong>change your password immediately</strong>.

View File

@@ -290,9 +290,6 @@
{% endset %}
{% include 'tables.headline.html.twig' %}
<br/>
{{ include('account/2fa/main.html.twig') }}
{{ hook('HOOK_ACCOUNT_MANAGE_BEFORE_ACCOUNT_LOGS') }}
<a name="Account+Logs" ></a>
<div class="TopButtonContainer">

View File

@@ -943,14 +943,6 @@ img {
font-size: 8pt;
color: red;
}
.AttentionSign img {
float: left;
top: 3px;
left: 8px;
width: 15px;
height: 13px;
margin-right: 5px;
}
.SmallBox {
position: relative;
font-size: 1px;