Compare commits

..

36 Commits

Author SHA1 Message Date
slawkens
ed88f9f401 Merge branch 'develop' into feature/2fa 2026-02-06 20:28:54 +01:00
slawkens
c2424df7a4 Rename the migration (its 51 now) 2026-01-31 21:46:55 +01:00
slawkens
4d2ed93b31 [WIP] 2fa - Add client-side login checks
Remove rfc6238.php, not used anymore
2026-01-31 21:41:22 +01:00
slawkens
7471c49793 [WIP] 2fa
* Don't allow per get request to disable 2fa
* Fix google recaptcha issue
* Fix rec key check
* Make input auth code required + autofocus
2026-01-31 20:44:26 +01:00
slawkens
381d5bb884 Merge branch 'develop' into feature/2fa 2026-01-31 19:28:43 +01:00
slawkens
234e17654b Add 51.php migration for 2fa 2026-01-31 17:45:54 +01:00
slawkens
1da771e3ca Merge branch 'develop' into feature/2fa 2026-01-31 17:45:40 +01:00
slawkens
4d7fe0bd58 Merge branch 'develop' into feature/2fa 2026-01-31 12:31:28 +01:00
slawkens
e2c9c2bbe0 Merge branch 'develop' into feature/2fa 2026-01-21 22:31:24 +01:00
slawkens
04b37b4356 Update enable.php 2026-01-21 20:58:30 +01:00
slawkens
bf70595095 Update composer dependencies 2026-01-21 20:56:33 +01:00
slawkens
668f00e746 Update enable.php 2026-01-21 20:56:17 +01:00
slawkens
bbc8bef008 Merge branch 'develop' into feature/2fa 2026-01-21 20:32:22 +01:00
slawkens
5e5fd43233 Fix phpstan 2026-01-21 20:14:58 +01:00
slawkens
867e3e2c38 [WIP] 2fa - Optimize code, views 2026-01-21 20:12:41 +01:00
slawkens
1975fb8ebe OTS_Account: setCustomField - Use Account model to update 2026-01-20 22:26:08 +01:00
slawkens
a44e2d6ebe Fix phpstan 2026-01-18 21:56:52 +01:00
slawkens
babd822171 New format of recovery key: xxxxx-xxxxx-xxxxx-xxxxx
TODO: adjust account lost recovery
2026-01-18 21:54:08 +01:00
slawkens
21e2eed640 [WIP] Working app auth (Still not ready)
Missing rec key validation
Doesn't work with google recaptcha plugin
2026-01-18 21:45:50 +01:00
slawkens
2e4a8c3d3d Add symfony/clock, required for spomky-labs/otphp 2026-01-18 13:14:28 +01:00
slawkens
9f64d7834f [WIP] 2fa, separate files, move twigs 2026-01-18 13:13:59 +01:00
slawkens
7d71bc2fee [WIP] 2fa Migration + column 2026-01-18 11:19:16 +01:00
slawkens
fdd0de8602 Merge branch 'develop' into feature/2fa 2026-01-18 11:13:36 +01:00
slawkens
abee4b3962 Add spomky-labs/otphp 2025-09-14 13:01:51 +02:00
slawkens
fbdb6890b9 Working two factor email authentication 2025-09-14 11:38:01 +02:00
slawkens
041f58ed11 Merge branch 'main' into feature/2fa 2025-09-14 09:53:34 +02:00
slawkens
03c7dd0002 Merge branch 'main' into feature/2fa 2025-08-12 14:36:29 +02:00
slawkens
e435062025 [WIP] 2fa 2025-07-05 08:20:58 +02:00
slawkens
ecc9bd4042 Merge branch 'main' into feature/2fa 2025-07-01 14:18:38 +02:00
slawkens
797377e428 Replace TwoFactorAuth with self 2025-06-22 22:34:36 +02:00
slawkens
96b5df9d74 Merge branch 'main' into feature/2fa 2025-06-22 18:51:32 +02:00
slawkens
b3dfc56c96 [WIP] Working 2fa email auth 2025-06-22 18:50:54 +02:00
slawkens
96d6e04bd2 Update 46-account_email_codes.sql 2025-06-22 13:19:29 +02:00
slawkens
9146eee327 Move 2025-06-22 11:55:34 +02:00
slawkens
3d97fa0719 Merge branch 'main' into feature/2fa 2025-06-22 11:45:21 +02:00
slawkens
a66cafceab 2fa: first draft 2025-06-22 08:34:30 +02:00
116 changed files with 2499 additions and 2563 deletions

View File

@@ -1,18 +1,5 @@
# Changelog
## [1.8.9 - 06.04.2026]
### Added
* Settings: Possibility to add custom HTML for the head and body tags like Google Analytics code etc. (https://github.com/slawkens/myaac/commit/108e83806df5686a06826931ed5e243c19cbe130)
* Add command: give-admin (https://github.com/slawkens/myaac/commit/9fa9ec746c4b344387a21f21886c2251319806fc)
* Usage: php aac give:admin slawkens@gmail.com
Parameter: account email, name or id
* It's admin for the website, not the GM for the game! For that, go into the admin panel and change the group manually
* Add page load time to an Admin Panel footer (https://github.com/slawkens/myaac/commit/4ae2fdd0dfcd56697612395c14aecc2dfd33b1c3)
### Changed
* Better character name validation, like in the original game website (#356)
* Install: don't suggest deleting of install folder - it's not required (https://github.com/slawkens/myaac/commit/5fcde4708a39255cf68edc8c43f2ac6597e2601d)
## [1.8.8 - 31.01.2026]
### Added
* Change Comment: Add missing hooks - patched from 0.8 (https://github.com/slawkens/myaac/commit/a60a23b84f61d41d1503073b52e01e3120f6d92a)

View File

@@ -1,12 +1,9 @@
## [2.0-dev - x.x.2025]
### Added
* Menus: Add an "access" option to Menus (#340)
* Add an "access" option to Menus (#340)
* Possibility to hide menus for unauthorized users
* Settings: Add Reset button (https://github.com/slawkens/myaac/commit/7104c2258fd724a55239821b46a616dab845b22a, https://github.com/slawkens/myaac/commit/e274b8350451a20c24e652ea05ed1964ebb86b54)
* New Setting: block create account spam by ip (https://github.com/slawkens/myaac/commit/54265f42e987522803288477952d6e5c4daeeb24)
* Functions: Add the possibility to fetch skills, balance and frags in the getTopPlayers function (#347)
* Plugins: autoload init-priority option (https://github.com/slawkens/myaac/commit/f1aa12840875960849fa0c99a2bbe0ad2949bbec)
* Add the possibility to fetch skills in the getTopPlayers function (#347)
### Changed
* Better handling of vocations: (#345)
@@ -14,7 +11,6 @@
* Support for Monk vocation
* Better gallery, loads images from images/gallery folder
* Reworked account action logs to use a single IP column as varchar(45) for both ipv4 and ipv6 (#289)
* Make myaac_config table columns bigger (https://github.com/slawkens/myaac/commit/2c62a97160a3ffe9976ee5bd1d770a0abc576742)
* Admin Panel: save menu collapse state (https://github.com/slawkens/myaac/commit/55da00520df7463a1d1ca41931df1598e9f2ffeb)
### Internal

View File

@@ -7,7 +7,7 @@ Official website: https://my-aac.org
[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/slawkens/myaac/cypress.yml)](https://github.com/slawkens/myaac/actions)
[![License: GPL-3.0](https://img.shields.io/github/license/slawkens/myaac)](https://opensource.org/licenses/gpl-license)
[![Downloads Count](https://img.shields.io/github/downloads/slawkens/myaac/total)](https://github.com/slawkens/myaac/releases)
[![MyAAC Discord](https://img.shields.io/discord/1468205461319848049)](https://discord.gg/aVagGPJt3g)
[![OpenTibia Discord](https://img.shields.io/discord/288399552581468162)](https://discord.gg/2J39Wus)
[![Closed Issues](https://img.shields.io/github/issues-closed-raw/slawkens/myaac)](https://github.com/slawkens/myaac/issues?q=is%3Aissue+is%3Aclosed)
| Version | Status | Branch | Requirements |
@@ -86,6 +86,12 @@ Look: [Contributing](https://docs.my-aac.org/misc/contributing) in our wiki.
If you have a great idea or want to contribute to the project - visit our website at https://www.my-aac.org
## Project supported by JetBrains
Many thanks to Jetbrains for kindly providing a license for me to work on this and other open-source projects.
[![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=https://github.com/slawkens)
### License
This program and all associated files are released under the GNU Public License.

View File

@@ -10,7 +10,6 @@
use MyAAC\Forum;
use MyAAC\Models\Player;
use MyAAC\Server\Outfits;
use MyAAC\Server\XML\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
@@ -660,14 +659,14 @@ else if (isset($_REQUEST['search'])) {
<div class="col-12 col-sm-12 col-lg-6">
<label for="look_type" class="control-label">Type:</label>
<?php
$outfits = Outfits::get();
if ($outfits) { ?>
$outfitlist = null;
$outfitlist = Outfits_loadfromXML();
if ($outfitlist) { ?>
<select name="look_type" id="look_type" class="form-control custom-select">
<?php
foreach ($outfits as $outfit) {
if ($outfit['enabled']) {
echo '<option value=' . $outfit['id'] . ($outfit['id'] == $player->getLookType() ? ' selected' : '') . '>' . $outfit['name'] . ' - ' . ($outfit['sex'] == SEX_MALE ? 'Male' : 'Female') . '</option>';
}
foreach ($outfitlist as $_id => $outfit) {
if ($outfit['enabled'] == 'yes') ;
echo '<option value=' . $outfit['id'] . ($outfit['id'] == $player->getLookType() ? ' selected' : '') . '>' . $outfit['name'] . ' - ' . ($outfit['type'] == 1 ? 'Male' : 'Female') . '</option>';
}
?>
</select>

View File

@@ -46,15 +46,6 @@ if (!is_array($settingsFile)) {
return;
}
if (isset($_POST['reset']) && $_POST['reset'] == '1') {
$settings = Settings::getInstance();
$settings->deleteFromDatabase($settingsFile['key']);
$settings->clearCache();
success('Settings for this plugin has been reset.');
}
$settingsKeyName = ($plugin == 'core' ? $plugin : $settingsFile['key']);
$title = ($plugin == 'core' ? 'Settings' : 'Plugin Settings - ' . $settingsFile['name']);
@@ -66,5 +57,4 @@ $twig->display('admin.settings.html.twig', [
'settings' => $settingsFile['settings'],
'script' => $settingsParsed['script'],
'settingsKeyName' => $settingsKeyName,
'pluginName' => $plugin,
]);

View File

@@ -172,8 +172,7 @@
<div class="float-sm-right d-none d-sm-inline">
<span class="p-2 right badge badge-<?php echo((isset($status['online']) and $status['online']) ? 'success' : 'danger'); ?>"><?php echo $config['lua']['serverName'] ?></span>
</div>
<?= base64_decode('UG93ZXJlZCBieSA8YSBocmVmPSJodHRwOi8vbXktYWFjLm9yZyIgdGFyZ2V0PSJfYmxhbmsiPk15QUFDLjwvYT4='); ?>
<?= 'Load time: ' . round(microtime(true) - START_TIME, 4) . ' seconds.'; ?>
<?php echo base64_decode('UG93ZXJlZCBieSA8YSBocmVmPSJodHRwOi8vbXktYWFjLm9yZyIgdGFyZ2V0PSJfYmxhbmsiPk15QUFDLjwvYT4='); ?>
</footer>
<div id="sidebar-overlay"></div>
</div>

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 = 52;
const DATABASE_VERSION = 51;
const TABLE_PREFIX = 'myaac_';
define('START_TIME', microtime(true));
define('MYAAC_OS', stripos(PHP_OS, 'WIN') === 0 ? 'WINDOWS' : (strtoupper(PHP_OS) === 'DARWIN' ? 'MAC' : 'LINUX'));
@@ -104,8 +104,6 @@ const OTSERV_FIRST = OTSERV;
const OTSERV_LAST = OTSERV_06;
const TFS_02 = 3;
const TFS_03 = 4;
const BLACKTEK_2 = 5;
const BLACKTEK = 6;
const TFS_FIRST = TFS_02;
const TFS_LAST = TFS_03;

View File

@@ -5,7 +5,6 @@
"ext-pdo_mysql": "*",
"ext-json": "*",
"ext-xml": "*",
"ext-simplexml": "*",
"ext-dom": "*",
"phpmailer/phpmailer": "^6.1",
"composer/semver": "^3.2",
@@ -21,7 +20,7 @@
"filp/whoops": "^2.15",
"maximebf/debugbar": "1.*",
"guzzlehttp/guzzle": "7.9.3",
"devium/toml": "^1.0"
"spomky-labs/otphp": "^11.3"
},
"require-dev": {
"phpstan/phpstan": "^1.10"

842
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,4 @@
<?php
use MyAAC\Server\Lua\Loader;
defined('MYAAC') or die('Direct access not allowed!');
if(!isset($_SESSION['var_server_path'])) {
@@ -14,18 +11,13 @@ $config['server_path'] = $_SESSION['var_server_path'];
if($config['server_path'][strlen($config['server_path']) - 1] != '/')
$config['server_path'] .= '/';
$configLuaExists = file_exists($config['server_path'] . 'config.lua');
$configTomlExists = file_exists($config['server_path'] . 'config/server.toml');
if((!isset($error) || !$error)
&& !$configLuaExists
&& !$configTomlExists) {
if((!isset($error) || !$error) && !file_exists($config['server_path'] . 'config.lua')) {
error($locale['step_database_error_config']);
$error = true;
}
if(!isset($error) || !$error) {
if($configLuaExists) {
$config['lua'] = Loader::load($config['server_path'] . 'config.lua');
$config['lua'] = load_config_lua($config['server_path'] . 'config.lua');
if(isset($config['lua']['sqlType'])) // tfs 0.3
$config['database_type'] = $config['lua']['sqlType'];
else if(isset($config['lua']['mysqlHost'])) // tfs 0.2/1.0
@@ -37,13 +29,6 @@ if (!isset($error) || !$error) {
else {
$config['database_type'] = '';
}
}
elseif ($configTomlExists) {
$tomlConfig = new MyAAC\Server\TOML\Config();
$tomlConfig->load();
$config['server'] = $tomlConfig->get();
$config['database_type'] = (isset($config['server']['database']['mysql']) ? 'mysql' : '');
}
$config['database_type'] = strtolower($config['database_type']);
if(empty($config['database_type'])) {

View File

@@ -5,9 +5,16 @@ CREATE TABLE IF NOT EXISTS `myaac_account_actions`
`ip` varchar(45) NOT NULL DEFAULT '',
`date` int NOT NULL DEFAULT 0,
`action` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
INDEX `myaac_account_actions_account_id` (`account_id`),
INDEX `myaac_account_actions_ip` (`ip`)
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`
@@ -45,8 +52,8 @@ CREATE TABLE IF NOT EXISTS `myaac_changelog`
CREATE TABLE IF NOT EXISTS `myaac_config`
(
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`value` varchar(10000) NOT NULL,
`name` varchar(30) NOT NULL,
`value` varchar(1000) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE (`name`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

View File

@@ -1,6 +1,5 @@
<?php
use MyAAC\Server\Config;
use Twig\Environment as Twig_Environment;
use Twig\Loader\FilesystemLoader as Twig_FilesystemLoader;
@@ -31,7 +30,7 @@ if(file_exists(CACHE . 'install.txt')) {
$install_status = unserialize(file_get_contents(CACHE . 'install.txt'));
if(!isset($_REQUEST['step'])) {
$step = $install_status['step'] ?? '';
$step = isset($install_status['step']) ? $install_status['step'] : '';
}
}
@@ -54,7 +53,7 @@ if($step == 'finish' && (!isset($config['installed']) || !$config['installed']))
// step verify
$steps = array(1 => 'welcome', 2 => 'license', 3 => 'requirements', 4 => 'config', 5 => 'database', 6 => 'admin', 7 => 'finish');
if(!in_array($step, $steps)) // check if a step is valid
if(!in_array($step, $steps)) // check if step is valid
throw new RuntimeException('ERROR: Unknown step.');
$install_status['step'] = $step;
@@ -62,7 +61,7 @@ $errors = array();
if($step == 'database') {
foreach($_SESSION as $key => $value) {
if(!str_contains($key, 'var_')) {
if(strpos($key, 'var_') === false) {
continue;
}
@@ -84,7 +83,7 @@ if($step == 'database') {
$config['server_path'] .= '/';
}
if(!Config::exists()) {
if(!file_exists($config['server_path'] . 'config.lua')) {
$errors[] = $locale['step_database_error_config'];
break;
}
@@ -183,7 +182,7 @@ $error = false;
clearstatcache();
if(is_writable(CACHE) && (MYAAC_OS != 'WINDOWS' || win_is_writable(CACHE))) {
if(!file_exists(BASE . 'install/ip.txt')) {
$content = warning('AAC installation is disabled. To enable it make a file <b>ip.txt</b> in install/ directory and put there your IP.<br/>
$content = warning('AAC installation is disabled. To enable it make file <b>ip.txt</b> in install/ directory and put there your IP.<br/>
Your IP is:<br /><b>' . get_browser_real_ip() . '</b>', true);
}
else {
@@ -199,7 +198,7 @@ if(is_writable(CACHE) && (MYAAC_OS != 'WINDOWS' || win_is_writable(CACHE))) {
if(!$allow)
{
$content = warning('In file <b>install/ip.txt</b> must be your IP!<br/>
In the file is:<br /><b>' . nl2br($file_content) . '</b><br/>
In file is:<br /><b>' . nl2br($file_content) . '</b><br/>
Your IP is:<br /><b>' . get_browser_real_ip() . '</b>', true);
}
else {

View File

@@ -98,6 +98,16 @@ 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...');

View File

@@ -30,8 +30,6 @@ $up();
DataLoader::setLocale($locale);
DataLoader::load();
clearCache();
// add menus entries
require_once SYSTEM . 'migrations/17.php';
$up();
@@ -69,10 +67,6 @@ if(file_exists(CACHE . 'install.txt')) {
unlink(CACHE . 'install.txt');
}
if(file_exists(BASE . 'install/ip.txt')) {
unlink(BASE . 'install/ip.txt');
}
$locale['step_finish_desc'] = str_replace('$ADMIN_PANEL$', generateLink(str_replace('tools/', '',ADMIN_URL), $locale['step_finish_admin_panel'], true), $locale['step_finish_desc']);
$locale['step_finish_desc'] = str_replace('$HOMEPAGE$', generateLink(str_replace('tools/', '', BASE_URL), $locale['step_finish_homepage'], true), $locale['step_finish_desc']);
$locale['step_finish_desc'] = str_replace('$LINK$', generateLink('https://my-aac.org', 'https://my-aac.org', true), $locale['step_finish_desc']);

View File

@@ -5,6 +5,7 @@ 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';
@@ -93,9 +94,9 @@ switch ($action) {
$creatureBoost = $db->query("SELECT * FROM " . $db->tableName('boosted_creature'))->fetchAll();
$bossBoost = $db->query("SELECT * FROM " . $db->tableName('boosted_boss'))->fetchAll();
die(json_encode([
//'boostedcreature' => true,
'bossraceid' => intval($bossBoost[0]['raceid']),
'boostedcreature' => true,
'creatureraceid' => intval($creatureBoost[0]['raceid']),
'bossraceid' => intval($bossBoost[0]['raceid'])
]));
}
@@ -108,17 +109,18 @@ switch ($action) {
case 'login':
$port = $config['lua']['gameProtocolPort'];
$ip = configLua('ip');
$port = configLua('gameProtocolPort');
// default world info
$world = [
'id' => 0,
'name' => $config['lua']['serverName'],
'externaladdress' => $config['lua']['ip'],
'externaladdress' => $ip,
'externalport' => $port,
'externaladdressprotected' => $config['lua']['ip'],
'externaladdressprotected' => $ip,
'externalportprotected' => $port,
'externaladdressunprotected' => $config['lua']['ip'],
'externaladdressunprotected' => $ip,
'externalportunprotected' => $port,
'previewstate' => 0,
'location' => 'BRA', // BRA, EUR, USA
@@ -133,13 +135,12 @@ switch ($action) {
$inputEmail = $request->email ?? false;
$inputAccountName = $request->accountname ?? false;
$inputToken = $request->token ?? false;
$account = Account::query();
if ($inputEmail != false) { // login by email
if ($inputEmail) { // login by email
$account->where('email', $inputEmail);
}
else if($inputAccountName != false) { // login by account name
else if($inputAccountName) { // login by account name
$account->where('name', $inputAccountName);
}
@@ -151,13 +152,14 @@ 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 != false ? 'Email' : 'Account name') . ' or password is not correct.');
sendError(($inputEmail ? 'Email' : 'Account name') . ' or password is not correct.');
}
$current_password = encrypt((USE_ACCOUNT_SALT ? $account->salt : '') . $request->password);
@@ -167,32 +169,30 @@ switch ($action) {
sendError($ban_msg);
}
sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.');
sendError(($inputEmail ? 'Email' : 'Account name') . ' or password is not correct.');
}
$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);
$twoFactorAuth = TwoFactorAuth::getInstance($account->id);
$code = '';
if ($twoFactorAuth->isActive()) {
if ($twoFactorAuth->getAuthType() === TwoFactorAuth::TYPE_EMAIL) {
$code = $request->emailcode ?? false;
}
sendError('Submit a valid two-factor authentication token.', 6);
} else {
require_once LIBS . 'rfc6238.php';
if (TokenAuth6238::verify($accountSecret, $inputToken) !== true) {
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('Two-factor authentication failed, token is wrong.', 6);
}
}
}
sendError($error, $errorCode);
}
$limiter->reset($ip);
@@ -220,46 +220,6 @@ 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');
@@ -268,7 +228,7 @@ switch ($action) {
if (!fieldExist('istutorial', 'players')) {
$sessionKey .= "\n";
}
$sessionKey .= ($accountHasSecret && strlen($accountSecret) > 5) ? $inputToken : '';
$sessionKey .= ($twoFactorAuth->isActive() && strlen($account->{'2fa_secret'}) > 5) ? $account->{'2fa_secret'} : '';
// this is workaround to distinguish between TFS 1.x and otservbr
// TFS 1.x requires the number in session key

12
package-lock.json generated
View File

@@ -1431,9 +1431,9 @@
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
@@ -1743,9 +1743,9 @@
}
},
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {

View File

@@ -4,7 +4,6 @@ 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

@@ -27,10 +27,12 @@ parameters:
- '#Variable \$player might not be defined#'
- '#Variable \$guild might not be defined#'
- '#Variable \$[a-zA-Z0-9\\_]+ might not be defined#'
- '#Class Lua constructor invoked with 0 parameters, 1 required#'
# Eloquent models
- '#Call to an undefined method [a-zA-Z0-9\\_]+::[a-zA-Z0-9\\_]+\(\)#'
- '#Call to an undefined static method [a-zA-Z0-9\\_]+::[a-zA-Z0-9\\_]+\(\)#'
# system/pages/highscores.php
- '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$online_status#'
- '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$vocation_name#'
-
message: '#Variable \$tmp in empty\(\) always exists and is always falsy#'
path: templates\kathrine\javascript.php

View File

@@ -9,8 +9,6 @@
*/
defined('MYAAC') or die('Direct access not allowed!');
use MyAAC\Server\Lua\Loader;
class Validator extends \MyAAC\Validator {}
function check_name($name, &$errors = '') {
@@ -76,77 +74,3 @@ function fieldExist($field, $table)
global $db;
return $db->hasColumn($table, $field);
}
function Outfits_loadfromXML(): ?array
{
global $config;
$file_path = $config['data_path'] . 'XML/outfits.xml';
if (!file_exists($file_path)) { return null; }
$xml = new DOMDocument;
$xml->load($file_path);
$outfits = null;
foreach ($xml->getElementsByTagName('outfit') as $outfit) {
$outfits[] = Outfit_parseNode($outfit);
}
return $outfits;
}
function Outfit_parseNode($node): array
{
$looktype = (int)$node->getAttribute('looktype');
$type = (int)$node->getAttribute('type');
$lookname = $node->getAttribute('name');
$premium = $node->getAttribute('premium');
$unlocked = $node->getAttribute('unlocked');
$enabled = $node->getAttribute('enabled');
return array('id' => $looktype, 'type' => $type, 'name' => $lookname, 'premium' => $premium, 'unlocked' => $unlocked, 'enabled' => $enabled);
}
function Mounts_loadfromXML(): ?array
{
global $config;
$file_path = $config['data_path'] . 'XML/mounts.xml';
if (!file_exists($file_path)) { return null; }
$xml = new DOMDocument;
$xml->load($file_path);
$mounts = null;
foreach ($xml->getElementsByTagName('mount') as $mount) {
$mounts[] = Mount_parseNode($mount);
}
return $mounts;
}
function Mount_parseNode($node): array
{
$id = (int)$node->getAttribute('id');
$clientid = (int)$node->getAttribute('clientid');
$name = $node->getAttribute('name');
$speed = (int)$node->getAttribute('speed');
$premium = $node->getAttribute('premium');
$type = $node->getAttribute('type');
return array('id' => $id, 'clientid' => $clientid, 'name' => $name, 'speed' => $speed, 'premium' => $premium, 'type' => $type);
}
function load_config_lua(string $file): array
{
$result = Loader::load($file);
if ($result === false) {
log_append('error.log', '[load_config_file] Fatal error: Cannot load config.lua (' . $file . ').');
throw new \RuntimeException('ERROR: Cannot find ' . $file . ' file.');
}
return $result;
}
function configLua($key) {
global $config;
if (is_array($key)) {
return $config['lua'][$key[0]] = $key[1];
}
return @$config['lua'][$key];
}

View File

@@ -18,19 +18,6 @@ if (!isset($config['database_overwrite'])) {
if(!$config['database_overwrite'] && !isset($config['database_user'][0], $config['database_password'][0], $config['database_name'][0]))
{
if (isset($config['server']['database']['mysql'])) { // BlackTek
$config['otserv_version'] = BLACKTEK;
$config['database_type'] = 'mysql';
$config['database_host'] = $config['server']['database']['mysql']['host'];
$config['database_port'] = $config['server']['database']['mysql']['port'];
$config['database_user'] = $config['server']['database']['mysql']['user'];
$config['database_password'] = $config['server']['database']['mysql']['pass'];
$config['database_name'] = $config['server']['database']['mysql']['database'];
if(!isset($config['database_socket'][0]) && !empty(trim($config['server']['database']['mysql']['socket']))) {
$config['database_socket'] = trim($config['server']['database']['mysql']['socket']);
}
$config['database_encryption'] = 'sha1';
}
if(isset($config['lua']['sqlType'])) {// tfs 0.3
if(isset($config['lua']['mysqlHost'])) {// tfs 0.2
$config['otserv_version'] = TFS_02;
@@ -107,6 +94,7 @@ if(!isset($config['database_socket'])) {
$config['database_socket'] = '';
}
try {
$ots->connect(array(
'host' => $config['database_host'],

View File

@@ -11,6 +11,7 @@ defined('MYAAC') or die('Direct access not allowed!');
use MyAAC\Cache\Cache;
use MyAAC\CsrfToken;
use MyAAC\Items;
use MyAAC\Models\Config;
use MyAAC\Models\Guild;
use MyAAC\Models\House;
@@ -20,7 +21,6 @@ use MyAAC\Models\PlayerDeath;
use MyAAC\Models\PlayerKillers;
use MyAAC\News;
use MyAAC\Plugins;
use MyAAC\Server\Items;
use MyAAC\Settings;
use PHPMailer\PHPMailer\PHPMailer;
@@ -987,6 +987,85 @@ function log_append($file, $str, array $params = [])
fclose($f);
}
function load_config_lua($filename)
{
global $config;
$config_file = $filename;
if(!@file_exists($config_file))
{
log_append('error.log', '[load_config_file] Fatal error: Cannot load config.lua (' . $filename . ').');
throw new RuntimeException('ERROR: Cannot find ' . $filename . ' file.');
}
$result = array();
$config_string = str_replace(array("\r\n", "\r"), "\n", file_get_contents($filename));
$lines = explode("\n", $config_string);
if(count($lines) > 0) {
foreach($lines as $ln => $line)
{
$line = trim($line);
if(isset($line[0]) && ($line[0] === '{' || $line[0] === '}')) {
// arrays are not supported yet
// just ignore the error
continue;
}
$tmp_exp = explode('=', $line, 2);
if(str_contains($line, 'dofile')) {
$delimiter = '"';
if(!str_contains($line, $delimiter)) {
$delimiter = "'";
}
$tmp = explode($delimiter, $line);
$result = array_merge($result, load_config_lua($config['server_path'] . $tmp[1]));
}
else if(count($tmp_exp) >= 2) {
$key = trim($tmp_exp[0]);
if(!str_starts_with($key, '--')) {
$value = trim($tmp_exp[1]);
if(str_contains($value, '--')) {// found some deep comment
$value = preg_replace('/--.*$/i', '', $value);
}
if(is_numeric($value))
$result[$key] = (float) $value;
elseif(in_array(@$value[0], array("'", '"')) && in_array(@$value[strlen($value) - 1], array("'", '"')))
$result[$key] = substr(substr($value, 1), 0, -1);
elseif(in_array($value, array('true', 'false')))
$result[$key] = $value === 'true';
elseif(@$value[0] === '{') {
// arrays are not supported yet
// just ignore the error
continue;
}
else
{
foreach($result as $tmp_key => $tmp_value) { // load values defined by other keys, like: dailyFragsToBlackSkull = dailyFragsToRedSkull
$value = str_replace($tmp_key, $tmp_value, $value);
}
try {
$ret = eval("return $value;");
}
catch (Throwable $e) {
throw new RuntimeException('ERROR: Loading config.lua file. Line: ' . ($ln + 1) . ' - Unable to parse value "' . $value . '" - ' . $e->getMessage());
}
if((string) $ret == '' && trim($value) !== '""') {
throw new RuntimeException('ERROR: Loading config.lua file. Line ' . ($ln + 1) . ' is not valid [key: ' . $key . ']');
}
$result[$key] = $ret;
}
}
}
}
}
return array_merge($result, $config['lua'] ?? []);
}
function str_replace_first($search,$replace, $subject) {
$pos = strpos($subject, $search);
if ($pos !== false) {
@@ -1246,6 +1325,15 @@ function config($key) {
return @$config[$key];
}
function configLua($key) {
global $config;
if (is_array($key)) {
return $config['lua'][$key[0]] = $key[1];
}
return @$config['lua'][$key];
}
function setting($key)
{
$settings = Settings::getInstance();
@@ -1552,6 +1640,58 @@ function verify_number($number, $name, $max_length)
echo_error($name . ' cannot be longer than ' . $max_length . ' digits.');
}
function Outfits_loadfromXML()
{
global $config;
$file_path = $config['data_path'] . 'XML/outfits.xml';
if (!file_exists($file_path)) { return null; }
$xml = new DOMDocument;
$xml->load($file_path);
$outfits = null;
foreach ($xml->getElementsByTagName('outfit') as $outfit) {
$outfits[] = Outfit_parseNode($outfit);
}
return $outfits;
}
function Outfit_parseNode($node) {
$looktype = (int)$node->getAttribute('looktype');
$type = (int)$node->getAttribute('type');
$lookname = $node->getAttribute('name');
$premium = $node->getAttribute('premium');
$unlocked = $node->getAttribute('unlocked');
$enabled = $node->getAttribute('enabled');
return array('id' => $looktype, 'type' => $type, 'name' => $lookname, 'premium' => $premium, 'unlocked' => $unlocked, 'enabled' => $enabled);
}
function Mounts_loadfromXML()
{
global $config;
$file_path = $config['data_path'] . 'XML/mounts.xml';
if (!file_exists($file_path)) { return null; }
$xml = new DOMDocument;
$xml->load($file_path);
$mounts = null;
foreach ($xml->getElementsByTagName('mount') as $mount) {
$mounts[] = Mount_parseNode($mount);
}
return $mounts;
}
function Mount_parseNode($node) {
$id = (int)$node->getAttribute('id');
$clientid = (int)$node->getAttribute('clientid');
$name = $node->getAttribute('name');
$speed = (int)$node->getAttribute('speed');
$premium = $node->getAttribute('premium');
$type = $node->getAttribute('type');
return array('id' => $id, 'clientid' => $clientid, 'name' => $name, 'speed' => $speed, 'premium' => $premium, 'type' => $type);
}
function left($str, $length) {
return substr($str, 0, $length);
}

View File

@@ -14,8 +14,7 @@ use MyAAC\CsrfToken;
use MyAAC\Hooks;
use MyAAC\Plugins;
use MyAAC\Models\Town;
use MyAAC\Server\Config;
use MyAAC\Server\Vocations;
use MyAAC\Server\XML\Vocations;
use MyAAC\Settings;
defined('MYAAC') or die('Direct access not allowed!');
@@ -91,20 +90,28 @@ foreach($_REQUEST as $var => $value) {
}
// load otserv config file
$config_lua_reload = true;
if($cache->enabled()) {
$tmp = null;
if(!$cache->fetch('server_path', $tmp) || $tmp != config('server_path')) {
$cache->delete('config_server');
if($cache->fetch('server_path', $tmp) && $tmp == $config['server_path']) {
$tmp = null;
if($cache->fetch('config_lua', $tmp) && $tmp) {
$config['lua'] = unserialize($tmp);
$config_lua_reload = false;
}
}
}
if (empty($config['server'])) {
$config['server'] = $config['lua'] = Config::get();
if($config_lua_reload) {
$config['lua'] = load_config_lua($config['server_path'] . 'config.lua');
// cache config
if($cache->enabled()) {
$cache->set('server_path', config('server_path'), 10 * 60);
$cache->set('config_lua', serialize($config['lua']), 2 * 60);
$cache->set('server_path', $config['server_path'], 10 * 60);
}
}
unset($tmp);
if(isset($config['lua']['servername']))
$config['lua']['serverName'] = $config['lua']['servername'];

View File

@@ -372,8 +372,8 @@ class POT
global $debugBar;
if (isset($debugBar)) {
$this->db = new \MyAAC\Debug\TraceablePDOWithBacktrace(new OTS_DB_MySQL($params));
$debugBar->addCollector(new \MyAAC\Debug\PDOCollectorWithBacktrace($this->db));
$this->db = new DebugBar\DataCollector\PDO\TraceablePDO(new OTS_DB_MySQL($params));
$debugBar->addCollector(new DebugBar\DataCollector\PDO\PDOCollector($this->db));
}
else {
$this->db = new OTS_DB_MySQL($params);

View File

@@ -736,17 +736,11 @@ 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();
}
// 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']);
AccountModel::where('id', $this->data['id'])->update([$field => $value]);
}
/**

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\Server\Groups;
use MyAAC\Cache\Cache;
/**
* List of groups.
@@ -47,9 +47,74 @@ class OTS_Groups_List implements IteratorAggregate, Countable
return;
}
foreach(Groups::get() as $id => $info) {
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();
}
}
}
/**

View File

@@ -1,284 +0,0 @@
<?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

@@ -20,7 +20,7 @@ $locale['not_loaded'] = 'Nicht geladen';
$locale['loading_spinner'] = 'Bitte warten, installieren...';
$locale['importing_spinner'] = 'Bitte warte, Daten werden importiert...';
$locale['please_fill_all'] = 'Bitte füllen Sie alle Felder aus!';
$locale['already_installed'] = 'MyAAC wurde bereits installiert. Wenn Sie MyAAC neu installieren möchten, löschen Sie die Datei <strong>config.local.php</strong> aus dem Hauptverzeichnis und aktualisieren Sie die Seite.';
$locale['already_installed'] = 'MyAAC wurde bereits installiert. Bitte löschen <b>install/</b> Verzeichnis. Wenn Sie MyAAC neu installieren möchten, löschen Sie die Datei <strong>config.local.php</strong> aus dem Hauptverzeichnis und aktualisieren Sie die Seite.';
// welcome
$locale['step_welcome'] = 'Willkommen';
@@ -67,7 +67,7 @@ $locale['step_database'] = 'Schema importieren';
$locale['step_database_title'] = 'MySQL schema importieren';
$locale['step_database_importing'] = 'Ihre Datenbank ist MySQL. Datenbankname ist: "$DATABASE_NAME$". Schema wird jetzt importiert...';
$locale['step_database_error_path'] = 'Bitte geben Sie den Serverpfad an.';
$locale['step_database_error_config'] = 'Datei config.lua oder config/server.toml kann nicht gefunden werden. Ist der Serverpfad korrekt? Gehen Sie zurück und überprüfen Sie noch einmal.';
$locale['step_database_error_config'] = 'Datei config.lua kann nicht gefunden werden. Ist der Serverpfad korrekt? Gehen Sie zurück und überprüfen Sie noch einmal.';
$locale['step_database_error_database_empty'] = 'Der Datenbanktyp kann nicht aus config.lua ermittelt werden. Ihr OTS wird von diesem AAC nicht unterstützt.';
$locale['step_database_error_only_mysql'] = 'Dieser AAC unterstützt nur MySQL. Aus Ihrer Konfigurationsdatei scheint Ihr OTS die Datenbank $DATABASE_TYPE$ zu verwenden. Bitte ändern Sie Ihre Datenbank in MySQL und folgen Sie dann der Installation erneut.';
$locale['step_database_error_table'] = 'Die Tabelle $TABLE$ existiert nicht. Bitte importieren Sie zuerst Ihr OTS-Datenbankschema.';

View File

@@ -20,7 +20,7 @@ $locale['not_loaded'] = 'Not loaded';
$locale['loading_spinner'] = 'Please wait, installing...';
$locale['importing_spinner'] = 'Please wait, importing data...';
$locale['please_fill_all'] = 'Please fill all inputs!';
$locale['already_installed'] = 'MyAAC has been already installed. If you want to reinstall MyAAC - please delete <strong>config.local.php</strong> file from the main directory and refresh the page.';
$locale['already_installed'] = 'MyAAC has been already installed. Please delete <b>install/</b> directory. If you want to reinstall MyAAC - please delete <strong>config.local.php</strong> file from the main directory and refresh the page.';
// welcome
$locale['step_welcome'] = 'Welcome';
@@ -72,7 +72,7 @@ $locale['step_database_title'] = 'Import MySQL schema';
$locale['step_database_importing'] = 'Your database is MySQL. Database name is: "$DATABASE_NAME$". Importing schema now...';
$locale['step_database_config_saved'] = 'Local configuration has been saved into file: config.local.php';
$locale['step_database_error_path'] = 'Please specify server path.';
$locale['step_database_error_config'] = 'Cannot find config.lua or config/server.toml file. Is your server path correct? Go back and check again.';
$locale['step_database_error_config'] = 'Cannot find config.lua file. Is your server path correct? Go back and check again.';
$locale['step_database_error_database_empty'] = 'Cannot determine database type from config.lua. Your OTS is unsupported by this AAC.';
$locale['step_database_error_only_mysql'] = 'This AAC supports only MySQL. From your config file it seems that your OTS is using: $DATABASE_TYPE$ database. Please change your database to MySQL and then follow the installation again.';
$locale['step_database_error_table'] = 'Table $TABLE$ doesn\'t exist. Please import your OTS database schema first.';

View File

@@ -20,7 +20,7 @@ $locale['not_loaded'] = 'Nie załadowane';
$locale['loading_spinner'] = 'Proszę czekać, trwa instalacja...';
$locale['importing_spinner'] = 'Proszę czekać, trwa importowanie danych...';
$locale['please_fill_all'] = 'Proszę wypełnić wszystkie pola!';
$locale['already_installed'] = 'MyAAC został już zainstalowany. Jeśli chcesz zainstalować MyAAC od nowa - proszę usuń plik <strong>config.local.php</strong> z katalogu głównego i odśwież stronę.';
$locale['already_installed'] = 'MyAAC został już zainstalowany. Proszę usunąć katalog <b>install/</b>. Jeśli chcesz zainstalować MyAAC od nowa - proszę usuń plik <strong>config.local.php</strong> z katalogu głównego i odśwież stronę.';
// welcome
$locale['step_welcome'] = 'Witamy';
@@ -71,7 +71,7 @@ $locale['step_database_title'] = 'Baza MySQL';
$locale['step_database_importing'] = 'Twoja baza to MySQL. Nazwa bazy danych to: "$DATABASE_NAME$". Importowanie schematu...';
$locale['step_database_config_saved'] = 'Lokalna konfiguracja została zapisana do pliku: config.local.php';
$locale['step_database_error_path'] = 'Proszę podać ścieżkę do serwera.';
$locale['step_database_error_config'] = 'Nie można znaleźć pliku config.lua lub config/server.toml. Czy ścieżka do katalogu serwera jest poprawna? Wróć się i sprawdź ponownie.';
$locale['step_database_error_config'] = 'Nie można znaleźć pliku config.lua. Czy ścieżka do katalogu serwera jest poprawna? Wróć się i sprawdź ponownie.';
$locale['step_database_error_database_empty'] = 'Nie można wykryć typu bazy danych z pliku config.lua. Prawdopodobnie Twój OTS nie jest wspierany przez ten AAC.';
$locale['step_database_error_only_mysql'] = 'Ten AAC wspiera tylko bazy danych MySQL. Z Twojego pliku config wynika, że Twój serwera używa bazy: $DATABASE_TYPE$. Proszę zmienić typ bazy na MySQL i ponownie przystąpić do instalacji.';
$locale['step_database_error_table'] = 'Tabela $TABLE$ nie istnieje. Proszę najpierw zaimportować schemat bazy danych serwera OTS.';

View File

@@ -20,7 +20,7 @@ $locale['not_loaded'] = 'Não carregado';
$locale['loading_spinner'] = 'Por favor aguarde, instalando...';
$locale['importing_spinner'] = 'Por favor, aguarde, importando dados...';
$locale['please_fill_all'] = 'Por favor, preencha todas as entradas!';
$locale['already_installed'] = 'MyAAC já foi instalado. Se você quiser reinstalar o MyAAC - exclua o arquivo <strong> config.local.php </strong> do diretório principal e atualize a página.';
$locale['already_installed'] = 'MyAAC já foi instalado. Por favor, apague o diretório <b> install/ <b/>. Se você quiser reinstalar o MyAAC - exclua o arquivo <strong> config.local.php </strong> do diretório principal e atualize a página.';
// welcome
$locale['step_welcome'] = 'Bem vindo';
@@ -61,7 +61,7 @@ $locale['step_database'] = 'Importar schema';
$locale['step_database_title'] = 'Importar MySQL schema';
$locale['step_database_importing'] = 'Seu banco de dados é o MySQL. O nome do banco de dados é: "$DATABASE_NAME$". Importando schema agora...';
$locale['step_database_error_path'] = 'Por favor, especifique o caminho da pasta do servidor.';
$locale['step_database_error_config'] = 'Não é possível encontrar o arquivo config.lua ou config/server.toml. O caminho da pasta do seu servidor está correto? Volte e verifique novamente.';
$locale['step_database_error_config'] = 'Não é possível encontrar o arquivo config.lua. O caminho da pasta do seu servidor está correto? Volte e verifique novamente.';
$locale['step_database_error_database_empty'] = 'Não é possível determinar o tipo de banco de dados a partir do config.lua. Seu OTS não é suportado por este AAC.';
$locale['step_database_error_only_mysql'] = 'Este AAC suporta apenas o MySQL. A partir do seu arquivo de configuração, parece que o seu OTS está usando: $DATABASE_TYPE$ database. Por favor, mude seu banco de dados para o MySQL e siga a instalação novamente.';
$locale['step_database_error_table'] = 'A tabela $TABLE$ não existe. Por favor, importe seu schema de banco de dados OTS primeiro.';

View File

@@ -18,7 +18,7 @@ $locale['loaded'] = 'Laddad';
$locale['not_loaded'] = 'Inte Laddad';
$locale['please_fill_all'] = 'Vänligen fyll i allt!';
$locale['already_installed'] = 'MyAAC är redan installerat. Om du vill installera MyAAC igen - ta bort filen <strong>config.local.php</strong> från huvudkatalogen och uppdatera sidan.';
$locale['already_installed'] = 'MyAAC är redan installerat. Vänligen ta bort <b>install/<b/> mappen. Om du vill installera MyAAC igen - ta bort filen <strong>config.local.php</strong> från huvudkatalogen och uppdatera sidan.';
// welcome
$locale['step_welcome'] = 'Välkommen';

View File

@@ -0,0 +1,8 @@
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,10 +1,36 @@
<?php
// 2fa
// add the myaac_account_email_codes
/**
* @var OTS_DB_MySQL $db
*/
$up = function () use ($db) {
$db->modifyColumn(TABLE_PREFIX . 'config', 'name', "varchar(255) NOT NULL");
$db->modifyColumn(TABLE_PREFIX . 'config', 'value', "varchar(10000) NOT NULL");
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 () {
// nothing to do, to not lose data
$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,13 +0,0 @@
<?php
/**
* 2026-04-12
* Add indexes to myaac_account_actions table
*/
$up = function () use ($db) {
$db->query("CREATE INDEX `myaac_account_actions_account_id` ON `myaac_account_actions` (`account_id`);");
$db->query("CREATE INDEX `myaac_account_actions_ip` ON `myaac_account_actions` (`ip`);");
};
$down = function () {
// nothing to do, to not lose data
};

View File

@@ -0,0 +1,26 @@
<?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

@@ -0,0 +1,105 @@
<?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

@@ -0,0 +1,41 @@
<?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

@@ -0,0 +1,34 @@
<?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

@@ -0,0 +1,51 @@
<?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

@@ -0,0 +1,32 @@
<?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,6 +17,10 @@ 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',
@@ -30,3 +34,11 @@ 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

@@ -171,7 +171,7 @@ if($save)
}
if(setting('core.account_create_character_create')) {
$character_name = isset($_POST['name']) ? trim(stripslashes($_POST['name'])) : null;
$character_name = isset($_POST['name']) ? stripslashes(ucwords(strtolower($_POST['name']))) : null;
$character_sex = isset($_POST['sex']) ? (int)$_POST['sex'] : null;
$character_vocation = isset($_POST['vocation']) ? (int)$_POST['vocation'] : null;
$character_town = isset($_POST['town']) ? (int)$_POST['town'] : null;

View File

@@ -10,6 +10,7 @@
*/
use MyAAC\RateLimit;
use MyAAC\TwoFactorAuth\TwoFactorAuth;
defined('MYAAC') or die('Direct access not allowed!');
@@ -52,8 +53,18 @@ 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 {
session_regenerate_id();
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('password', encrypt((USE_ACCOUNT_SALT ? $account_logged->getCustomField('salt') : '') . $login_password));
if($remember_me) {
setSession('remember_me', true);

View File

@@ -8,6 +8,9 @@
* @copyright 2019 MyAAC
* @link https://my-aac.org
*/
use MyAAC\TwoFactorAuth\TwoFactorAuth;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Account Management';
@@ -116,6 +119,8 @@ $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 = generateRandomString(10, false, true, true);
$new_rec_key = generateRecoveryKey();
$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 = generateRandomString(10, false, true, true);
$new_rec_key = generateRecoveryKey();
$account_logged->setCustomField("key", $new_rec_key);
$account_logged->logAction('Generated recovery key.');

View File

@@ -11,22 +11,22 @@
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Experience Stages';
if((!isset($config['lua']['experienceStages']) || !getBoolean($config['lua']['experienceStages']))
&& (!isset($config['lua']['rateUseStages']) || !getBoolean($config['lua']['rateUseStages']))
) {
$enabled = false;
if(file_exists($config['data_path'] . 'XML/stages.xml')) {
$stages = new DOMDocument();
$stages->load($config['data_path'] . 'XML/stages.xml');
}
if(!isset($config['lua']['experienceStages']) || !getBoolean($config['lua']['experienceStages']))
{
$enabled = false;
if(isset($stages)) {
foreach($stages->getElementsByTagName('config') as $node) {
/** @var DOMElement $node */
if($node->getAttribute('enabled')) {
if($node->getAttribute('enabled'))
$enabled = true;
}
}
}
if(!$enabled) {
$rate_exp = 'not set';
@@ -42,12 +42,21 @@ if((!isset($config['lua']['experienceStages']) || !getBoolean($config['lua']['ex
}
}
$stages = new MyAAC\Server\ExpStages();
$stagesArray = $stages->get();
if (empty($stagesArray)) {
echo 'Error when loading experience stages.';
if(!$stages)
{
echo 'Error: cannot load <b>stages.xml</b>!';
return;
}
$stagesArray = [];
foreach($stages->getElementsByTagName('stage') as $stage)
{
/** @var DOMElement $stage */
$maxLevel = $stage->getAttribute('maxlevel');
$stagesArray[] = [
'levels' => $stage->getAttribute('minlevel') . (isset($maxLevel[0]) ? '-' . $maxLevel : '+'),
'multiplier' => $stage->getAttribute('multiplier')
];
}
$twig->display('experience_stages.html.twig', ['stages' => $stagesArray]);

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\Vocations;
use MyAAC\Server\XML\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Highscores';
@@ -207,14 +207,10 @@ if (empty($highscores)) {
}
$highscores = $query->get()->map(function($row) {
/**
* @var Player $row
*/
$tmp = $row->toArray();
$tmp['online'] = $row->online_status;
$tmp['vocation'] = $row->vocation_name;
$tmp['outfit_url'] = $row->outfit_url;
$tmp['link'] = getPlayerLink($row->name, false);
$tmp['outfit_url'] = $row->outfit_url; // @phpstan-ignore-line
unset($tmp['online_table']);
return $tmp;
@@ -248,6 +244,7 @@ foreach($highscores as $id => &$player)
$player['experience'] = number_format($player['experience']);
}
$player['link'] = getPlayerLink($player['name'], false);
$player['flag'] = getFlagImage($player['country']);
$player['outfit'] = '<img style="position:absolute;margin-top:-50px;margin-left:-30px" src="' . $player['outfit_url'] . '" alt="" />';

View File

@@ -12,7 +12,7 @@
use MyAAC\Cache\Cache;
use MyAAC\Models\ServerConfig;
use MyAAC\Models\ServerRecord;
use MyAAC\Server\Vocations;
use MyAAC\Server\XML\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Who is online?';
@@ -87,16 +87,13 @@ $cached = Cache::remember("online_$order", setting('core.online_cache_ttl') * 60
'name' => getPlayerLink($player['name']),
'player' => $player,
'level' => $player['level'],
'vocation' => $configVocations[$player['vocation']] ?? 'Unknown',
'vocation' => $configVocations[$player['vocation']],
'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'],
);
$originalId = Vocations::getOriginal($player['vocation']);
if ($originalId) {
$vocations[$originalId]++;
}
$vocations[Vocations::getOriginal($player['vocation'])]++;
}
$record = '';

View File

@@ -12,7 +12,6 @@
*/
use MyAAC\Cache;
use MyAAC\Server\Config;
use MyAAC\Settings;
$templates = Cache::remember('templates', 5 * 60, function () {
@@ -1803,8 +1802,8 @@ Sent by MyAAC,<br/>
// test config.lua existence
// if fail - revert the setting and inform the user
if (!Config::exists()) {
error('Server Path is invalid - cannot find config.lua or config/server.toml in the directory. Setting have been reverted.');
if (!file_exists($server_path . 'config.lua')) {
error('Server Path is invalid - cannot find config.lua in the directory. Setting have been reverted.');
$configToSave['server_path'] = $configOriginal['server_path'];
}

View File

@@ -2,7 +2,6 @@
namespace MyAAC\Commands;
use MyAAC\Server\Config;
use POT;
trait Env
@@ -22,7 +21,7 @@ trait Env
if($config['server_path'][strlen($config['server_path']) - 1] !== '/')
$config['server_path'] .= '/';
$config['server'] = $config['lua'] = Config::get();
$config['lua'] = load_config_lua($config['server_path'] . 'config.lua');
// POT
require_once SYSTEM . 'libs/pot/OTS.php';

View File

@@ -39,7 +39,7 @@ class GiveAdminCommand extends Command
}
if (!$account->isLoaded()) {
$io->error('Cannot find account with supplied parameter: ' . $accountParam);
$io->error('Cannot find account mit supplied parameter: ' . $accountParam);
return self::FAILURE;
}

View File

@@ -27,7 +27,6 @@ namespace MyAAC;
use MyAAC\Cache\Cache;
use MyAAC\Models\Town;
use MyAAC\Server\Items;
class DataLoader
{
@@ -41,7 +40,7 @@ class DataLoader
{
self::$startTime = microtime(true);
if(Items::load()) {
if(Items::loadFromXML()) {
success(self::$locale['step_database_loaded_items'] . self::getLoadedTime());
}
else {

View File

@@ -1,52 +0,0 @@
<?php
namespace MyAAC\Debug;
use DebugBar\DataCollector\PDO\PDOCollector;
use DebugBar\DataCollector\PDO\TraceablePDO;
use DebugBar\DataCollector\TimeDataCollector;
class PDOCollectorWithBacktrace extends PDOCollector
{
protected function collectPDO(TraceablePDO $pdo, ?TimeDataCollector $timeCollector = null, $connectionName = null): array
{
$data = parent::collectPDO($pdo, $timeCollector, $connectionName);
if ($pdo instanceof TraceablePDOWithBacktrace) {
$backtraces = $pdo->getBacktraces();
foreach ($data['statements'] as $i => &$stmt) {
if (isset($backtraces[$i])) {
$stmt['backtrace'] = $this->formatBacktrace($backtraces[$i]);
}
}
unset($stmt);
}
return $data;
}
private function formatBacktrace(array $backtrace): array
{
$result = [];
foreach ($backtrace as $frame) {
if (!isset($frame['file'], $frame['line'])) {
continue;
}
if (str_contains($frame['file'], DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) {
continue;
}
if (str_contains($frame['file'], DIRECTORY_SEPARATOR . 'Debug' . DIRECTORY_SEPARATOR)) {
continue;
}
$function = isset($frame['class'])
? $frame['class'] . ($frame['type'] ?? '::') . ($frame['function'] ?? '')
: ($frame['function'] ?? '');
$result[] = ($function ? $function . '() ' : '') . $frame['file'] . ':' . $frame['line'];
}
return $result;
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace MyAAC\Debug;
use DebugBar\DataCollector\PDO\TraceablePDO;
use DebugBar\DataCollector\PDO\TracedStatement;
class TraceablePDOWithBacktrace extends TraceablePDO
{
/** @var array[] */
protected array $backtraces = [];
public function addExecutedStatement(TracedStatement $stmt): void
{
$this->backtraces[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
parent::addExecutedStatement($stmt);
}
/**
* @return array[]
*/
public function getBacktraces(): array
{
return $this->backtraces;
}
}

View File

@@ -14,26 +14,6 @@ class Hooks
self::$_hooks[$hook->type()][] = $hook;
}
public function unregister($name, $type, $file): void
{
if (is_string($type)) {
$type = constant($type);
}
if(!isset(self::$_hooks[$type])) {
return;
}
foreach(self::$_hooks[$type] as $id => $hook) {
if($name == $hook->name()
&& $type == $hook->type()
&& $file == $hook->file()
) {
unset(self::$_hooks[$type][$id]);
}
}
}
public function trigger($type, $params = []): bool
{
$ret = true;

View File

@@ -1,14 +1,172 @@
<?php
/**
* @deprecated
* This class is deprecated and will be removed in future versions. Please use the appropriate MyAAC\Server\Items class instead.
* Items class
*
* @package MyAAC
* @author Gesior <jerzyskalski@wp.pl>
* @author Slawkens <slawkens@gmail.com>
* @copyright 2019 MyAAC
* @link https://my-aac.org
*/
namespace MyAAC;
class Items extends Server\Items
use MyAAC\Cache\PHP as CachePHP;
use MyAAC\Models\Spell;
class Items
{
public static function load(): bool {
parent::init();
private static $error = '';
public static $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;
}
$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() {
return self::$error;
}
public static function load() {
if(self::$items) {
return;
}
$cache_php = new CachePHP(config('cache_prefix'), CACHE . 'persistent/');
self::$items = $cache_php->get('items');
}
public static function get($id) {
self::load();
return self::$items[$id] ?? [];
}
public static function getDescription($id, $count = 1): string
{
$item = self::get($id);
$attr = $item['attributes'];
$s = '';
if(!empty($item['name'])) {
if($count > 1) {
if($attr['showcount']) {
$s .= $count . ' ';
}
if(!empty($item['plural'])) {
$s .= $item['plural'];
}
else if((int)$attr['showcount'] == 0) {
$s .= $item['name'];
}
else {
$s .= $item['name'] . 's';
}
}
else {
if(!empty($item['aticle'])) {
$s .= $item['article'] . ' ';
}
$s .= $item['name'];
}
}
else
$s .= 'an item of type ' . $item['id'];
if(isset($attr['type']) && strtolower($attr['type']) == 'rune') {
$spell = Spell::where('item_id', $id)->first();
if($spell) {
if($spell->level > 0 && $spell->maglevel > 0) {
$s .= '. ' . ($count > 1 ? 'They' : 'It') . ' can only be used by ';
}
$configVocations = config('vocations');
if(!empty(trim($spell->vocations))) {
$vocations = json_decode($spell->vocations);
if(count($vocations) > 0) {
foreach($vocations as $voc => $show) {
$vocations[$configVocations[$voc]] = $show;
}
}
}
else {
$s .= 'players';
}
$s .= ' with';
if ($spell->level > 0) {
$s .= ' level ' . $spell->level;
}
if ($spell->maglevel > 0) {
if ($spell->level > 0) {
$s .= ' and';
}
$s .= ' magic level ' . $spell->maglevel;
}
$s .= ' or higher';
}
}
if (!empty($item['weaponType'])) {
if ($item['weaponType'] == 'distance' && isset($item['ammoType'])) {
$s .= ' (Range:' . $item['range'];
}
if (isset($item['attack']) && $item['attack'] != 0) {
$s .= ', Atk ' . ($item['attack'] > 0 ? '+' . $item['attack'] : '-' . $item['attack']);
}
if (isset($item['hitChance']) && $item['hitChance'] != -1) {
$s .= ', Hit% ' . ($item['hitChance'] > 0 ? '+' . $item['hitChance'] : '-' . $item['hitChance']);
}
elseif ($item['weaponType'] != 'ammo') {
}
}
return $s;
}
}

View File

@@ -18,15 +18,6 @@ class Account extends Model {
public $timestamps = false;
protected $fillable = [
'name', 'number', 'email', 'password',
'key', 'created', 'rlname', 'location', 'country',
'web_lastlogin', 'web_flags',
'email_new', 'email_new_time', 'email_code',
'premium_points', 'coins', 'coins_transferable',
'premium_ends_at', 'premend', 'lastday', 'premdays',
];
protected $casts = [
'lastday' => 'integer',
'premdays' => 'integer',

View File

@@ -0,0 +1,14 @@
<?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

@@ -0,0 +1,15 @@
<?php
namespace MyAAC\Models;
use Illuminate\Database\Eloquent\Model;
class BugTracker extends Model {
protected $table = TABLE_PREFIX . 'bugtracker';
public $timestamps = false;
protected $fillable = ['account', 'type', 'status', 'text', 'id', 'subject', 'reply', 'who', 'uid', 'tag'];
}

View File

@@ -5,10 +5,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* @property string $name
* @property int $level
* @property int $vocation
* @property int $promotion
* @property int $online
* @property int $looktype
* @property int $lookhead
@@ -16,8 +14,6 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property int $looklegs
* @property int $lookfeet
* @property int $lookaddons
* @property bool $online_status
* @property string $vocation_name
* @property string $outfit_url
* @property hasOne $onlineTable
*/

View File

@@ -12,11 +12,13 @@
namespace MyAAC;
use MyAAC\Models\Monster;
use MyAAC\Server\Items;
class Monsters {
private static \OTS_MonstersList $monstersList;
private static string $lastError = '';
/**
* @var \OTS_MonstersList
*/
private static $monstersList;
private static $lastError = '';
public static function loadFromXML($show = false) {
try {
@@ -37,7 +39,7 @@ class Monsters {
}
$items = array();
Items::init();
Items::load();
foreach((array)Items::$items as $id => $item) {
$items[$item['name']] = $id;
}

View File

@@ -7,11 +7,9 @@ use MyAAC\Cache\Cache;
use MyAAC\Models\Menu;
class Plugins {
private static array $warnings = [];
private static string $error = '';
private static array $plugin_json = [];
const DEFAULT_PRIORITY = 1000;
private static $warnings = [];
private static $error = null;
private static $plugin_json = [];
public static function getInits()
{
@@ -22,31 +20,13 @@ class Plugins {
continue;
}
$initPriority = self::DEFAULT_PRIORITY;
if (isset($plugin['autoload']['init-priority'])) {
$initPriority = (int) $plugin['autoload']['init-priority'];
}
$pluginInits = glob(PLUGINS . $plugin['filename'] . '/init.php');
foreach ($pluginInits as $path) {
$inits[] = [
'file' => $path,
'priority' => $initPriority
];
$inits[] = $path;
}
}
usort($inits, function ($a, $b)
{
return $a['priority'] <=> $b['priority'];
});
$ret = [];
foreach ($inits as $init) {
$ret[] = $init['file'];
}
return $ret;
return $inits;
});
}
@@ -59,7 +39,7 @@ class Plugins {
continue;
}
$adminPagesDefaultPriority = self::DEFAULT_PRIORITY;
$adminPagesDefaultPriority = 1000;
if (isset($plugin['admin-pages-default-priority'])) {
$adminPagesDefaultPriority = $plugin['admin-pages-default-priority'];
}
@@ -137,7 +117,7 @@ class Plugins {
$routes = [];
foreach(self::getAllPluginsJson() as $plugin) {
$routesDefaultPriority = self::DEFAULT_PRIORITY;
$routesDefaultPriority = 1000;
if (isset($plugin['routes-default-priority'])) {
$routesDefaultPriority = $plugin['routes-default-priority'];
}
@@ -185,7 +165,7 @@ class Plugins {
}
}
$pagesDefaultPriority = self::DEFAULT_PRIORITY;
$pagesDefaultPriority = 1000;
if (isset($plugin['pages-default-priority'])) {
$pagesDefaultPriority = $plugin['pages-default-priority'];
}
@@ -338,7 +318,7 @@ class Plugins {
foreach(self::getAllPluginsJson() as $plugin) {
if (isset($plugin['hooks'])) {
foreach ($plugin['hooks'] as $_name => $info) {
$priority = self::DEFAULT_PRIORITY;
$priority = 1000;
if (str_contains($info['type'], 'HOOK_')) {
$info['type'] = str_replace('HOOK_', '', $info['type']);
@@ -452,7 +432,7 @@ class Plugins {
return $plugins;
}
public static function getPluginSettings($filename): mixed
public static function getPluginSettings($filename)
{
$plugin_json = self::getPluginJson($filename);
if (!$plugin_json) {
@@ -888,15 +868,6 @@ class Plugins {
}
}
global $hooks;
foreach($plugin_info['hooks'] ?? [] as $name => $info) {
if (str_contains($info['type'], 'HOOK_')) {
$info['type'] = str_replace('HOOK_', '', $info['type']);
}
$hooks->unregister($name, 'HOOK_' . $info['type'], $info['file']);
}
clearCache();
return true;
}
@@ -921,15 +892,15 @@ class Plugins {
return Semver::satisfies($plugin_info['version'], $version);
}
public static function getWarnings(): array {
public static function getWarnings() {
return self::$warnings;
}
public static function clearWarnings(): void {
public static function clearWarnings() {
self::$warnings = [];
}
public static function getError(): string {
public static function getError() {
return self::$error;
}
@@ -940,7 +911,7 @@ class Plugins {
* @param string $templateName
* @param array $menus
*/
public static function installMenus($templateName, $menus, $clearOld = false): void
public static function installMenus($templateName, $menus, $clearOld = false)
{
global $db;
@@ -991,7 +962,7 @@ class Plugins {
}
}
private static function getAutoLoadOption(array $plugin, string $optionName, bool $default = true): bool
private static function getAutoLoadOption(array $plugin, string $optionName, bool $default = true)
{
if (isset($plugin['autoload'])) {
$autoload = $plugin['autoload'];

View File

@@ -1,28 +0,0 @@
<?php
namespace MyAAC\Server;
use MyAAC\Cache\Cache;
class Config
{
public static function get()
{
return Cache::remember('config_server', 10 * 60, function () {
if (file_exists(config('server_path') . Lua\Config::FILE)) {
$config = new Lua\Config();
}
else {
$config = new TOML\Config();
}
$config->load();
return $config->get();
});
}
public static function exists(): bool {
return file_exists(config('server_path') . Lua\Config::FILE) || file_exists(config('server_path') . 'config/server.toml');
}
}

View File

@@ -1,35 +0,0 @@
<?php
namespace MyAAC\Server;
use MyAAC\Cache\Cache;
class ExpStages
{
private static array $stages = [];
public static function get() {
if (count(self::$stages) == 0) {
self::$stages = Cache::remember('exp_stages', 10 * 60, function () {
if (file_exists(config('server_path') . TOML\ExpStages::FILE)) {
$expStages = new TOML\ExpStages();
}
elseif (file_exists(config('data_path') . XML\ExpStages::FILE)) {
$expStages = new XML\ExpStages();
}
elseif (file_exists(config('data_path') . Lua\ExpStages::FILE)) {
$expStages = new Lua\ExpStages();
}
else {
return [];
}
$expStages->load();
return $expStages->get();
});
}
return self::$stages;
}
}

View File

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

View File

@@ -1,154 +0,0 @@
<?php
/**
* Items class
*
* @package MyAAC
* @author Gesior <jerzyskalski@wp.pl>
* @author Slawkens <slawkens@gmail.com>
* @copyright 2019 MyAAC
* @link https://my-aac.org
*/
namespace MyAAC\Server;
use MyAAC\Cache\PHP as CachePHP;
use MyAAC\Models\Spell;
class Items
{
public static array $items = [];
private static string $error = '';
const FILE_ITEMS_TOML = 'items/items.toml';
const FILE_ITEMS_XML = 'items/items.xml';
public static function getError(): string {
return self::$error;
}
public static function load(): bool {
if (file_exists(config('data_path') . self::FILE_ITEMS_TOML)) {
$items = new TOML\Items();
}
elseif (file_exists(config('data_path') . self::FILE_ITEMS_XML)) {
$items = new 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;
}
$cache_php = new CachePHP(config('cache_prefix'), CACHE . 'persistent/');
self::$items = (array)$cache_php->get('items');
}
public static function get($id) {
self::init();
return self::$items[$id] ?? [];
}
public static function getDescription($id, $count = 1): string
{
$item = self::get($id);
$attr = $item['attributes'];
$s = '';
if(!empty($item['name'])) {
if($count > 1) {
if($attr['showcount']) {
$s .= $count . ' ';
}
if(!empty($item['plural'])) {
$s .= $item['plural'];
}
else if((int)$attr['showcount'] == 0) {
$s .= $item['name'];
}
else {
$s .= $item['name'] . 's';
}
}
else {
if(!empty($item['aticle'])) {
$s .= $item['article'] . ' ';
}
$s .= $item['name'];
}
}
else
$s .= 'an item of type ' . $item['id'];
if(isset($attr['type']) && strtolower($attr['type']) == 'rune') {
$spell = Spell::where('item_id', $id)->first();
if($spell) {
if($spell->level > 0 && $spell->maglevel > 0) {
$s .= '. ' . ($count > 1 ? 'They' : 'It') . ' can only be used by ';
}
$configVocations = config('vocations');
if(!empty(trim($spell->vocations))) {
$vocations = json_decode($spell->vocations);
if(count($vocations) > 0) {
foreach($vocations as $voc => $show) {
$vocations[$configVocations[$voc]] = $show;
}
}
}
else {
$s .= 'players';
}
$s .= ' with';
if ($spell->level > 0) {
$s .= ' level ' . $spell->level;
}
if ($spell->maglevel > 0) {
if ($spell->level > 0) {
$s .= ' and';
}
$s .= ' magic level ' . $spell->maglevel;
}
$s .= ' or higher';
}
}
if (!empty($item['weaponType'])) {
if ($item['weaponType'] == 'distance' && isset($item['ammoType'])) {
$s .= ' (Range:' . $item['range'];
}
if (isset($item['attack']) && $item['attack'] != 0) {
$s .= ', Atk ' . ($item['attack'] > 0 ? '+' . $item['attack'] : '-' . $item['attack']);
}
if (isset($item['hitChance']) && $item['hitChance'] != -1) {
$s .= ', Hit% ' . ($item['hitChance'] > 0 ? '+' . $item['hitChance'] : '-' . $item['hitChance']);
}
elseif ($item['weaponType'] != 'ammo') {
}
}
return $s;
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace MyAAC\Server\Lua;
class Config
{
const FILE = 'config.lua';
private array $config = [];
public function load(): void
{
$file = config('server_path') . self::FILE;
$this->config = Loader::load($file);
if($this->config === false) {
log_append('error.log', '[Config] Fatal error: Cannot load config.lua (' . $file . ').');
throw new \RuntimeException('ERROR: Cannot find ' . $file . ' file.');
}
}
public function get(): array {
return $this->config;
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace MyAAC\Server\Lua;
class ExpStages
{
private array $stages = [];
const FILE = 'stages.lua';
public function load(): void
{
$file = config('data_path') . self::FILE;
if(!@file_exists($file)) {
return;
}
if (!extension_loaded('lua')) {
return;
}
$lua = new \Lua();
try {
$stagesContent = file_get_contents($file);
$stagesContent .= 'return experienceStages';
$stages = $lua->eval($stagesContent);
}
catch (\Exception $e) {
error('Error: Cannot load stages.lua. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load stages.lua - $file. Error: " . $e->getMessage());
return;
}
foreach ($stages as $stage) {
$this->stages[] = [
'levels' => $stage['minlevel'] . (isset($stage['maxlevel']) ? '-' . $stage['maxlevel'] : '+'),
'multiplier' => $stage['multiplier']
];
}
}
public function get(): array {
return $this->stages;
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace MyAAC\Server\Lua;
class Loader
{
public static function load($file): bool|array
{
if(!@file_exists($file)){
return false;
}
$result = [];
$config_string = str_replace(array("\r\n", "\r"), "\n", file_get_contents($file));
$lines = explode("\n", $config_string);
if(count($lines) > 0) {
foreach($lines as $ln => $line) {
$line = trim($line);
if(isset($line[0]) && ($line[0] === '{' || $line[0] === '}')) {
// arrays are not supported yet
// just ignore the error
continue;
}
$tmp_exp = explode('=', $line, 2);
if(str_contains($line, 'dofile')) {
$delimiter = '"';
if(!str_contains($line, $delimiter)) {
$delimiter = "'";
}
$tmp = explode($delimiter, $line);
$result = array_merge($result, self::load(config('server_path') . $tmp[1]));
}
else if(count($tmp_exp) >= 2) {
$key = trim($tmp_exp[0]);
if(!str_starts_with($key, '--')) {
$value = trim($tmp_exp[1]);
if(str_contains($value, '--')) {// found some deep comment
$value = preg_replace('/--.*$/i', '', $value);
}
if(is_numeric($value))
$result[$key] = (float) $value;
elseif(in_array(@$value[0], array("'", '"')) && in_array(@$value[strlen($value) - 1], array("'", '"')))
$result[$key] = substr(substr($value, 1), 0, -1);
elseif(in_array($value, array('true', 'false')))
$result[$key] = $value === 'true';
elseif(@$value[0] === '{') {
// arrays are not supported yet
// just ignore the error
continue;
}
else
{
foreach($result as $tmp_key => $tmp_value) { // load values defined by other keys, like: dailyFragsToBlackSkull = dailyFragsToRedSkull
$value = str_replace($tmp_key, $tmp_value, $value);
}
try {
$ret = eval("return $value;");
}
catch (\Throwable $e) {
throw new \RuntimeException('ERROR: Loading config.lua file. Line: ' . ($ln + 1) . ' - Unable to parse value "' . $value . '" - ' . $e->getMessage());
}
if((string) $ret == '' && trim($value) !== '""') {
throw new \RuntimeException('ERROR: Loading config.lua file. Line ' . ($ln + 1) . ' is not valid [key: ' . $key . ']');
}
$result[$key] = $ret;
}
}
}
}
}
return $result;
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace MyAAC\Server;
use MyAAC\Cache\Cache;
class Mounts
{
private static array $mounts = [];
public static function get()
{
if (count(self::$mounts) == 0) {
self::$mounts = Cache::remember('mounts', 10 * 60, function () {
if (file_exists(config('server_path') . TOML\Mounts::FILE)) {
$mounts = new TOML\Mounts();
}
else {
$mounts = new XML\Mounts();
}
$mounts->load();
return $mounts->get();
});
}
return self::$mounts;
}
}

View File

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

View File

@@ -1,69 +0,0 @@
<?php
namespace MyAAC\Server\TOML;
use Devium\Toml\Toml;
use RuntimeException;
class Config
{
private array $config = [];
public function load(): void
{
$path = config('server_path') . 'config/';
$files = glob($path . '*.toml');
// filter files we don't need
$ignore = ['account_manager', 'groups', 'mounts', 'object_pools', 'outfits', 'scripts'];
$files = array_filter($files, function ($file) use ($ignore) {
foreach ($ignore as $item) {
if (str_contains($file, $item)) {
return false;
}
}
return true;
});
foreach ($files as $file) {
$key = basename($file, '.toml');
$toml = file_get_contents($file);
try {
$this->config[$key] = Toml::decode($toml, asArray: true);
}
catch (\Exception $e) {
throw new RuntimeException("Error: Cannot load config/$key.toml. More info in system/logs/error.log file.");
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load config/$key.toml - $file. Error: " . $e->getMessage());
return;
}
}
$this->init();
}
private function init(): void
{
$this->config['serverName'] = $this->config['server']['identity']['name'] ?? 'Unknown';
$this->config['freePremium'] = $this->config['server']['accounts']['free_premium'] ?? false;
$this->config['ip'] = $this->config['server']['network']['ip'] ?? '127.0.0.1';
$this->config['worldType'] = $this->config['server']['world']['type'] ?? 'unknown';
$this->config['experienceStages'] = $this->config['stages']['config']['enabled'] ?? false;
$this->config['houseRentPeriod'] = $this->config['server']['houses']['rent_period'] ?? 'never';
$this->config['pzLocked'] = $this->config['combat']['skull']['pz_locked'] ?? 60 * 1000;
$this->config['url'] = $this->config['server']['identity']['url'] ?? 'http://localhost';
$this->config['protectionLevel'] = $this->config['server']['pvp']['protection_level'] ?? 0;
$this->config['rateExp'] = $this->config['rates']['rates']['experience'] ?? 1;
$this->config['rateMagic'] = $this->config['rates']['rates']['magic'] ?? 1;
$this->config['rateSkill'] = $this->config['rates']['rates']['skill'] ?? 1;
$this->config['rateLoot'] = $this->config['rates']['rates']['loot'] ?? 1;
$this->config['rateSpawn'] = $this->config['rates']['rates']['spawn'] ?? 1;
}
public function get(): array {
return $this->config;
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace MyAAC\Server\TOML;
use Devium\Toml\Toml;
class ExpStages
{
private array $stages = [];
const FILE = 'config/stages.toml';
public function load(): void
{
$file = config('server_path') . self::FILE;
if(!@file_exists($file)) {
return;
}
$toml = file_get_contents($file);
try {
$stages = Toml::decode($toml, asArray: true);
}
catch (\Exception $e) {
error('Error: Cannot load stages.toml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load stages.toml - $file. Error: " . $e->getMessage());
return;
}
foreach ($stages['stage'] as $stage) {
$this->stages[] = [
'levels' => $stage['minlevel'] . (isset($stage['maxlevel']) ? '-' . $stage['maxlevel'] : '+'),
'multiplier' => $stage['multiplier']
];
}
}
public function get(): array {
return $this->stages;
}
}

View File

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

View File

@@ -1,52 +0,0 @@
<?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 = config('data_path') . 'items/items.toml';
if (!file_exists($file)) {
$this->error = 'Cannot load file ' . $file;
return false;
}
//$toml = file_get_contents($file);
//$items = \Devium\Toml\Toml::decode($toml, asArray: false);
$itemsParser = new ItemsParser();
$itemsParsed = $itemsParser->parse($file);
$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;
}
$items[$id] = [
'article' => $item['article'] ?? '',
'name' => $item['name'] ?? '',
'plural' => $item['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

@@ -1,47 +0,0 @@
<?php
namespace MyAAC\Server\TOML;
class ItemsParser
{
public function parse(string $path): array
{
$ret = [];
$i = 0;
$handle = fopen($path, 'r');
if ($handle === false) {
throw new \RuntimeException('Failed to open items file: ' . $path);
}
$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_starts_with(ltrim($line), 'field =')) {
continue;
}
$parse .= $line;
}
if ($parse !== '') {
$ret[] = parse_ini_string($parse);
}
fclose($handle);
return $ret;
}
}

View File

@@ -1,47 +0,0 @@
<?php
namespace MyAAC\Server\TOML;
use Devium\Toml\Toml;
class Mounts
{
private array $mounts = [];
const FILE = 'config/mounts.toml';
public function load(): void
{
$file = config('server_path') . self::FILE;
if(!@file_exists($file)) {
return;
}
$toml = file_get_contents($file);
try {
$mounts = Toml::decode($toml, asArray: true);
}
catch (\Exception $e) {
error('Error: Cannot load mounts.toml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load mounts.toml - $file. Error: " . $e->getMessage());
return;
}
foreach ($mounts as $name => $mount)
{
$this->mounts[] = [
'id' => $mount['id'],
'client_id' => $mount['clientid'] ?? false,
'name' => $name,
'speed' => $mount['speed'] ?? 0,
'premium' => $mount['premium'] ?? false,
];
}
}
public function get(): array {
return $this->mounts;
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace MyAAC\Server\TOML;
use Devium\Toml\Toml;
class Outfits
{
private array $outfits = [];
const FILE = 'config/outfits.toml';
public function load(): void
{
$file = config('server_path') . self::FILE;
if(!@file_exists($file)) {
return;
}
$toml = file_get_contents($file);
try {
$outfits = Toml::decode($toml, asArray: true);
}
catch (\Exception $e) {
error('Error: Cannot load outfits.toml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load outfits.toml - $file. Error: " . $e->getMessage());
return;
}
foreach ($outfits as $outfit)
{
$this->outfits[] = [
'id' => $outfit['id'],
'sex' => ($outfit['sex'] == 'male' ? SEX_MALE : SEX_FEMALE),
'name' => $outfit['name'],
'premium' => $outfit['premium'] ?? false,
'locked' => $outfit['locked'] ?? false,
'enabled' => $outfit['enabled'] ?? true,
];
}
}
public function get(): array {
return $this->outfits;
}
}

View File

@@ -1,51 +0,0 @@
<?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);
try {
$vocations = Toml::decode($toml, asArray: true);
}
catch (\Exception $e) {
$basename = basename($file);
error("Error: Cannot load vocations/$basename. More info in system/logs/error.log file.");
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load mounts.toml - $file. Error: " . $e->getMessage());
break;
}
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

@@ -1,86 +0,0 @@
<?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

@@ -1,40 +0,0 @@
<?php
namespace MyAAC\Server\XML;
class ExpStages
{
private array $stages = [];
const FILE = 'XML/stages.xml';
public function load(): void
{
$file = config('data_path') . self::FILE;
if(!@file_exists($file)) {
return;
}
$xml = new \DOMDocument();
if(!$xml->load($file)) {
error('Error: Cannot load stages.xml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load stages.xml - $file. Error: " . print_r(error_get_last(), true));
return;
}
foreach($xml->getElementsByTagName('stage') as $stage)
{
/** @var \DOMElement $stage */
$maxLevel = $stage->getAttribute('maxlevel');
$this->stages[] = [
'levels' => $stage->getAttribute('minlevel') . (isset($maxLevel[0]) ? '-' . $maxLevel : '+'),
'multiplier' => $stage->getAttribute('multiplier')
];
}
}
public function get(): array {
return $this->stages;
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace MyAAC\Server\XML;
class Groups
{
private array $groups = [];
const FILE = 'XML/groups.xml';
public function load(): void
{
$file = config('data_path') . self::FILE;
if(!@file_exists($file)) {
error('Error: Cannot load groups.xml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] 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', "[" . __CLASS__ . "] 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 get(): array {
return $this->groups;
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace MyAAC\Server\XML;
use MyAAC\Cache\PHP as CachePHP;
class Items
{
private string $error = '';
const FILE = 'items/items.xml';
public function getError(): string {
return $this->error;
}
public function load(): bool
{
$file = config('data_path') . self::FILE;
if (!file_exists($file)) {
$this->error = 'Cannot load file ' . $file;
return false;
}
$items = [];
try {
$xml = new \SimpleXMLElement(file_get_contents($file));
} catch (\Exception $e) {
$this->error = 'Error: Cannot load items.xml. More info in system/logs/error.log file.';
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load items.xml - $file. Error: " . $e->getMessage());
return false;
}
foreach($xml->xpath('item') as $item) {
if ($item->attributes()->fromid) {
for ($id = (int)$item->attributes()->fromid; $id <= (int)$item->attributes()->toid; $id++) {
$tmp = $this->parseNode($id, $item);
$items[$tmp['id']] = $tmp['content'];
}
} else {
$tmp = $this->parseNode($item->attributes()->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->attributes()->name;
$article = $node->attributes()->article;
$plural = $node->attributes()->plural;
$attributes = [];
foreach($node->xpath('attribute') as $attr) {
$attributes[strtolower($attr->attributes()->key)] = (string)$attr->attributes()->value;
if ($attr->xpath('attribute')) {
foreach($attr->xpath('attribute') as $attr2) {
$attributes[strtolower($attr2->attributes()->key)] = (string)$attr2->attributes()->value;
}
}
}
return [
'id' => (int)$id,
'content' => [
'article' => (string)$article,
'name' => (string)$name,
'plural' => (string)$plural,
'attributes' => $attributes
],
];
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace MyAAC\Server\XML;
class Mounts
{
private array $mounts = [];
const FILE = 'XML/mounts.xml';
public function load(): void
{
$file = config('data_path') . self::FILE;
if(!@file_exists($file)) {
return;
}
$xml = new \DOMDocument();
if(!$xml->load($file)) {
error('Error: Cannot load mounts.xml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load mounts.xml - $file. Error: " . print_r(error_get_last(), true));
return;
}
foreach ($xml->getElementsByTagName('mount') as $mount) {
$this->mounts[] = $this->parseMountNode($mount);
}
}
private function parseMountNode($node): array
{
$id = (int)$node->getAttribute('id');
$client_id = (int)$node->getAttribute('clientid');
$name = $node->getAttribute('name');
$speed = (int)$node->getAttribute('speed');
$premium = getBoolean($node->getAttribute('premium'));
$type = $node->getAttribute('type');
return [
'id' => $id,
'client_id' => $client_id,
'name' => $name,
'speed' => $speed,
'premium' => $premium,
'type' => $type
];
}
public function get(): array {
return $this->mounts;
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace MyAAC\Server\XML;
class Outfits
{
private array $outfits = [];
const FILE = 'XML/outfits.xml';
public function load(): void
{
$file = config('data_path') . self::FILE;
if(!@file_exists($file)) {
return;
}
$xml = new \DOMDocument();
if(!$xml->load($file)) {
error('Error: Cannot load outfits.xml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load outfits.xml - $file. Error: " . print_r(error_get_last(), true));
return;
}
foreach ($xml->getElementsByTagName('outfit') as $outfit) {
$this->outfits[] = $this->parseOutfitNode($outfit);
}
}
private function parseOutfitNode($node): array
{
$looktype = (int)$node->getAttribute('looktype');
$type = (int)$node->getAttribute('type');
$name = $node->getAttribute('name');
$premium = getBoolean($node->getAttribute('premium'));
$locked = !getBoolean($node->getAttribute('unlocked'));
$enabled = getBoolean($node->getAttribute('enabled'));
return [
'id' => $looktype,
'sex' => ($type === 1 ? SEX_MALE : SEX_FEMALE),
'name' => $name,
'premium' => $premium,
'locked' => $locked,
'enabled' => $enabled,
];
}
public function get(): array {
return $this->outfits;
}
}

View File

@@ -2,42 +2,100 @@
namespace MyAAC\Server\XML;
use MyAAC\Cache\Cache;
class Vocations
{
private array $vocations = [];
private array $vocationsFrom = [];
private static array $vocations;
private static array $vocationsFrom;
const FILE = 'vocations.xml';
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']]);
}
public function load(): void
{
$file = config('data_path') . 'XML/' . self::FILE;
if(!class_exists('DOMDocument')) {
throw new \RuntimeException('Please install PHP xml extension. MyAAC will not work without it.');
}
$vocationsXML = new \DOMDocument();
$file = config('data_path') . 'XML/vocations.xml';
if(!@file_exists($file)) {
$file = config('data_path') . self::FILE;
$file = config('data_path') . 'vocations.xml';
}
$xml = new \DOMDocument();
if(!$xml->load($file)) {
error('Error: Cannot load vocations.xml. More info in system/logs/error.log file.');
log_append('error.log', "[" . __CLASS__ . "] Fatal error: Cannot load vocations.xml - $file. Error: " . print_r(error_get_last(), true));
return;
if(!$vocationsXML->load($file)) {
throw new \RuntimeException('ERROR: Cannot load <i>vocations.xml</i> - the file is malformed. Check the file with xml syntax validator.');
}
foreach($xml->getElementsByTagName('vocation') as $vocation) {
foreach($vocationsXML->getElementsByTagName('vocation') as $vocation) {
$id = $vocation->getAttribute('id');
$this->vocations[$id] = $vocation->getAttribute('name');
self::$vocations[$id] = $vocation->getAttribute('name');
$fromVocation = (int) $vocation->getAttribute('fromvoc');
$this->vocationsFrom[$id] = $fromVocation;
self::$vocationsFrom[$id] = $fromVocation;
}
}
public function get(): array {
return $this->vocations;
public static function get(): array {
return self::$vocations;
}
public function getFrom(): array {
return $this->vocationsFrom;
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 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

@@ -367,7 +367,6 @@ class Settings implements \ArrayAccess
</div>
<div class="box-footer">
<button name="save" type="submit" class="btn btn-primary">Save</button>
<button form="reset-settings-form" name="reset" type="submit" class="btn btn-warning position-absolute" style="right: 0; bottom: 0" onclick="return confirm('Are you sure? This will clear all settings for this plugin!')">Reset</button>
</div>
<?php

View File

@@ -0,0 +1,19 @@
<?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

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

View File

@@ -0,0 +1,16 @@
<?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

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

View File

@@ -0,0 +1,270 @@
<?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

@@ -183,7 +183,7 @@ class Validator
return false;
}
// installer doesn't know settings yet
// installer doesn't know config.php yet
// that's why we need to ignore the nulls
if(defined('MYAAC_INSTALL')) {
$minLength = 4;
@@ -207,15 +207,21 @@ class Validator
return false;
}
if(strspn($name, "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM ") != $length)
if(strspn($name, "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM- [ ] '") != $length)
{
self::$lastError = "This name contains invalid letters. Please use only A-Z, a-z and space!";
self::$lastError = "Invalid name format. Use only A-Z, spaces and '.";
return false;
}
if(preg_match('/ {2,}/', $name))
{
self::$lastError = 'Invalid character name format. Use only A-Z, a-z and no double spaces.';
self::$lastError = 'Invalid character name format. Use only A-Z and no double spaces.';
return false;
}
if(!preg_match("/[A-z ']/", $name))
{
self::$lastError = "Invalid name format. Use only A-Z, spaces and '.";
return false;
}
@@ -224,23 +230,17 @@ class Validator
/**
* Validate new character name.
* Name length must be 3-25 chars
* Name lenght must be 3-25 chars
*
* @param string $name Name to check
* @return bool Is name valid?
*/
public static function newCharacterName($name)
{
global $db;
global $db, $config;
$name = trim($name);
$name_lower = strtolower($name);
if(strlen($name) < 1) {
self::$lastError = 'Please enter a name.';
return false;
}
$first_words_blocked = array_merge(["'", '-'], setting('core.create_character_name_blocked_prefix'));
foreach($first_words_blocked as $word) {
if($word == substr($name_lower, 0, strlen($word))) {
@@ -249,6 +249,11 @@ class Validator
}
}
if(str_ends_with($name_lower, "'") || str_ends_with($name_lower, "-")) {
self::$lastError = 'Your name contains illegal characters.';
return false;
}
if(substr($name_lower, 1, 1) == ' ') {
self::$lastError = 'Your name contains illegal space.';
return false;
@@ -260,36 +265,11 @@ class Validator
}
if(preg_match('/ {2,}/', $name)) {
self::$lastError = 'Invalid character name format. Use only A-Z and no double spaces.';
self::$lastError = 'Invalid character name format. Use only A-Z and numbers 0-9 and no double spaces.';
return false;
}
if (substr($name[0], 0, 1) !== strtoupper(substr($name[0], 0, 1))) {
self::$lastError = 'The first letter of a name has to be a capital letter.';
return false;
}
foreach (explode(' ', $name) as $word) {
$wordCut = substr($word, 1, strlen($word));
$hasUpperCase = preg_match('/[A-Z]/', $wordCut);
if ($hasUpperCase) {
self::$lastError = 'In names capital letters are only allowed at the beginning of a word.';
return false;
}
if (strlen($word) == 1) {
self::$lastError = 'This name contains a word with only one letter. Please use more than one letter for each word.';
return false;
}
$hasVowel = preg_match('/[aeiouAEIOU]/', $word);
if (!$hasVowel) {
self::$lastError = 'This name contains a word without vowels. Please choose another name.';
return false;
}
}
if(strtolower(configLua('serverName')) == $name_lower) {
if(strtolower($config['lua']['serverName']) == $name_lower) {
self::$lastError = 'Your name cannot be same as server name.';
return false;
}

View File

@@ -1,8 +1,5 @@
<?php
const SEX_FEMALE = 0;
const SEX_MALE = 1;
const SKILL_FRAGS = -1;
const SKILL_BALANCE = -2;
@@ -72,6 +69,7 @@ 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,6 +147,9 @@
{% 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

@@ -0,0 +1,76 @@
<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

@@ -0,0 +1,101 @@
{% 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

@@ -0,0 +1,64 @@
{% 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

@@ -0,0 +1,25 @@
<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>

Some files were not shown because too many files have changed in this diff Show More