Compare commits

...

122 Commits

Author SHA1 Message Date
slawkens
ed88f9f401 Merge branch 'develop' into feature/2fa 2026-02-06 20:28:54 +01:00
slawkens
54265f42e9 New Setting: block create account spam by ip 2026-02-06 20:26:14 +01:00
slawkens
bd87881ad0 Update create.php 2026-02-06 20:20:49 +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
7a113ee72a Settings: Possibility to navigate tabs through link 2026-01-31 19:26:35 +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
8d78e7090d Better gallery
Replaced complex gallery with simple script
Slideshow loaded from images/gallery folder
Credits: https://www.w3schools.com/howto/howto_js_slideshow.asp
2026-01-31 17:42:59 +01:00
slawkens
fd457b2fe8 Merge branch 'main' into develop 2026-01-31 16:01:46 +01:00
slawkens
108e83806d Settings: Custom HTML for <head> and <body> 2026-01-31 16:01:34 +01:00
slawkens
85e8d4d9af Merge branch 'main' into develop 2026-01-31 15:50:08 +01:00
slawkens
9d6287ecbc Settings: Use current year for the footer, instead of predefined one 2026-01-31 15:49:25 +01:00
slawkens
16f4cdf40a Merge branch 'main' into develop 2026-01-31 15:36:45 +01:00
slawkens
9fa9ec746c Add give:admin Command
Usage: php aac give:admin slawkens@gmail.com
Parameter: account email, name or id
2026-01-31 15:29:48 +01:00
slawkens
3e7ee12676 Update news.html.twig 2026-01-31 15:07:49 +01:00
slawkens
b435a2fba4 Update CHANGELOG-2.x.md 2026-01-31 12:37:50 +01:00
slawkens
4d7fe0bd58 Merge branch 'develop' into feature/2fa 2026-01-31 12:31:28 +01:00
slawkens
d91de1005b Merge branch 'main' into develop 2026-01-31 12:31:02 +01:00
slawkens
88ea9ceee1 Create AccountBan.php 2026-01-31 12:30:07 +01:00
slawkens
a92428287d Migration: 49 - Fix get proper account id for samples 2026-01-31 12:12:32 +01:00
slawkens
9c8a78a386 Merge branch 'main' into develop 2026-01-31 11:46:03 +01:00
slawkens
ff4b15ad1d Merge branch 'main' into develop 2026-01-30 16:43:48 +01:00
slawkens
eaa8d9346e Fix migration 49.php when there is no session 2026-01-29 20:44:28 +01:00
slawkens
3e2d4d6686 Merge branch 'main' into develop 2026-01-28 21:59:46 +01:00
slawkens
e2c9c2bbe0 Merge branch 'develop' into feature/2fa 2026-01-21 22:31:24 +01:00
slawkens
87509ffe16 Refactor OTS_Account save(), fixing premium days on canary 2026-01-21 22:31:13 +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
8d7c36e3eb Merge branch 'main' into develop 2026-01-21 20:31:48 +01:00
slawkens
1edb4743fe Fix phpstan php version matrix 2026-01-21 20:20:55 +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
e3efbdc5a8 Add php 8.5 to phpstan workflow 2026-01-17 19:34:48 +01:00
slawkens
d4cc47e341 Add tfs-0.3 to github workflow cypress 2026-01-17 19:21:50 +01:00
Slawomir Boczek
5040a93031 Feature/refactor account lost (#326)
* [WIP] Account Lost refactor

* [WIP] Refactor account/lost

* Update form.html.twig

* Use myaac-table class for tables

* Set $title to 'Lost Account'

* Remove duplicated code - extract lostAccountCooldown function

* [WIP] Add csrfProtect()

* Refactor code, better $error messages

* Formatting

* Refactor

Add missing password check
Formatting

* [WIP] Refactor

* [WIP] Refactor account lost

* [WIP] Refactor account lost - fixes

* [WIP] Account lost refactor

* Fixes
* Add account lost hooks for password strength plugin
2026-01-17 18:59:01 +01:00
slawkens
173b1ace88 Update CHANGELOG-2.x.md 2026-01-17 00:19:35 +01:00
Slawomir Boczek
276aa600e2 Feature/ots player rewrite (#348)
* [WIP] Rewrite OTS_Player class

* Fix exception on load a non existing player

* Fix for servers that don't have the cap & conditions columns

* Fix created column on player save

* Update OTS_Player.php

* Add Monk Sample + fixes

* Move FAQ creation to import_base_data + cleanup
2026-01-16 23:18:03 +01:00
slawkens
8103f5e70f Merge branch 'main' into develop 2026-01-15 21:47:07 +01:00
slawkens
8632cd3191 Add setting core.vocations for backward compatibility 2026-01-14 19:44:56 +01:00
slawkens
8b6f160a0f Fix php cache get return type 2026-01-12 18:59:51 +01:00
slawkens
c3036e7d49 Don't show account name for server that don't have it
Admin Panel -> Accounts Editor
2026-01-04 16:22:07 +01:00
slawkens
0c4edf625c Fix for servers with promotion column (mostly tfs 0.3+) 2026-01-04 15:30:23 +01:00
slawkens
c28dc29391 Use develop branch for github workflows 2026-01-04 13:33:44 +01:00
slawkens
2db4f6a57b Update online.html.twig 2026-01-04 13:19:58 +01:00
slawkens
9bfd0242af Make vocation a bit smaller
To fit into the theme
2026-01-04 13:18:09 +01:00
slawkens
3ea2b68561 Update CHANGELOG-2.x.md 2026-01-04 13:17:03 +01:00
Slawomir Boczek
dcdaa5ef43 Feature/get top players skills (#347)
* Add skills to getTopPlayers

* Add example top-5

* Extract getSkillIdByName($name)
2026-01-04 13:13:38 +01:00
slawkens
7289cce826 Update CHANGELOG-2.x.md 2026-01-04 13:13:14 +01:00
slawkens
af9d4c2aeb Update CHANGELOG-2.x.md 2026-01-04 13:04:01 +01:00
Slawomir Boczek
a66edfad31 Restore vocations.xml loading + support for Monk (#345)
* Restore vocations.xml loading

For better handling of vocations
Monk is supported now

* New images for vocations (+ added Monk)

* Fix online.html.twig cause of merge
2026-01-04 13:00:34 +01:00
slawkens
89a35b5335 Merge branch 'main' into develop 2026-01-04 12:41:11 +01:00
slawkens
55da00520d Admin Panel: save menu collapse state 2026-01-03 22:04:49 +01:00
slawkens
efef16ee86 Extract script.ajax-setup.html.twig 2026-01-03 22:01:58 +01:00
slawkens
2f0b67f840 Merge branch 'main' into develop 2026-01-03 20:40:56 +01:00
slawkens
ca2e3bb576 Merge branch 'main' into develop 2026-01-03 13:21:22 +01:00
slawkens
e0e0e46701 Move forum show_board code to Twig 2026-01-02 13:30:35 +01:00
slawkens
61bcdc0c37 Merge branch 'main' into develop 2026-01-01 13:23:12 +01:00
slawkens
f966dff5a8 Convert switch to match 2025-12-28 15:50:10 +01:00
Slawomir Boczek
402f3bb9b0 [WIP] Add access option to Menus (#340)
* [WIP] Add access option to Menus

Thanks @joelslamospersson for idea

* Add notice about Guest*

* Add access column into schema.sql

* Remove spectrum.js from project

Was used in Menus, replaced by html "color" input

* Block access to page if not required Access by Menus
2025-12-26 12:59:49 +01:00
slawkens
e98de451d8 Merge branch 'main' into develop 2025-12-22 20:04:31 +01:00
slawkens
4fffaf6aff Merge branch 'main' into develop 2025-12-18 14:33:45 +01:00
slawkens
c44c9f9cf4 Add type hints and return types to cache classes 2025-12-18 14:33:07 +01:00
slawkens
ccfd6f1a87 Add PHP to cache engine list in settings 2025-12-18 14:23:25 +01:00
slawkens
96b8e00f49 Refactor PHP cache to store expiration and improve typing
Cache entries now store both the value and expiration timestamp in the file, allowing for more reliable expiration checks. Method signatures have been updated with type hints.
2025-12-18 14:22:42 +01:00
slawkens
11cb1cf97e Save db cache only if it has changed 2025-12-18 11:53:06 +01:00
slawkens
c5d3d3a25f Merge branch 'main' into develop 2025-12-14 10:21:33 +01:00
slawkens
e1197515f3 Merge branch 'main' into develop 2025-11-23 10:13:00 +01:00
slawkens
eebfc600cb Detect "deletion" column in guilds show 2025-11-18 00:15:32 +01:00
slawkens
9a99018dce Merge branch 'main' into develop 2025-11-13 20:08:38 +01:00
slawkens
6479546c22 Update CHANGELOG-2.x.md 2025-10-31 16:23:41 +01:00
slawkens
effb23f367 Create CHANGELOG-2.x.md 2025-10-31 15:32:49 +01:00
slawkens
08657c1599 Fix migration 47.php (convert IPs) 2025-10-31 15:25:55 +01:00
slawkens
1379c93439 Create 47.php 2025-10-31 07:00:11 +01:00
slawkens
19b1cfdd34 Merge branch 'main' into develop 2025-10-31 06:56:34 +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
slawkens
e719725841 Merge branch 'main' into develop 2025-05-09 13:45:54 +02:00
slawkens
bb3e90110d Merge branch 'main' into develop 2025-05-09 13:14:12 +02:00
slawkens
2f0758e351 Update schema.sql 2025-04-26 06:17:58 +02:00
slawkens
6667c8c364 Merge branch 'main' into develop 2025-04-26 06:17:38 +02:00
slawkens
c13a540878 Merge branch 'main' into develop 2025-04-18 13:58:42 +02:00
slawkens
869ec035d9 Merge branch 'main' into develop 2025-04-04 21:09:12 +02:00
slawkens
9d696d31d8 Merge branch 'main' into develop 2025-04-04 20:08:24 +02:00
slawkens
8cc4caf587 Merge branch 'main' into develop 2025-04-01 07:43:57 +02:00
slawkens
e1d1c7d5db Merge branch 'main' into develop 2025-03-31 22:21:16 +02:00
slawkens
320733c2c1 Merge branch 'main' into develop 2025-03-31 19:51:21 +02:00
slawkens
c1809a98d1 Merge branch 'main' into develop 2025-03-30 07:11:15 +02:00
slawkens
46ed541015 Merge branch 'main' into develop 2025-03-16 20:54:40 +01:00
slawkens
29207361b7 Merge branch 'main' into develop 2025-03-16 12:39:32 +01:00
slawkens
25013ae91b Merge branch 'main' into develop 2025-03-15 23:09:14 +01:00
slawkens
5d630ba9dd Fix the second "Save" button -> addition to previous commit 2025-03-15 22:49:43 +01:00
slawkens
feadf1314d Fix: add possibility to remove all menu items 2025-03-15 22:49:37 +01:00
slawkens
08b8a716d4 Fix the second "Save" button -> addition to previous commit 2025-03-10 13:04:57 +01:00
slawkens
cc26b5c744 Fix: add possibility to remove all menu items 2025-03-10 10:48:19 +01:00
Slawomir Boczek
cb6e9a6a88 Feature/twig hooks filters (#258)
* feat: Hooks filters

* Cleanup
2025-03-09 21:39:37 +01:00
slawkens
4adb0758c5 Set version to 2.0-dev 2025-03-09 21:26:24 +01:00
Slawomir Boczek
7312383f73 Account actions rework on ip (Use single column for IP - VARCHAR(45)) (#289)
* Account actions rework on ip (Use single column for IP - VARCHAR(45))

* No foreach needed here
2025-03-09 21:18:12 +01:00
slawkens
3c1210fefa Nothing important, just better code style 2025-03-03 20:07:54 +01:00
143 changed files with 4411 additions and 5267 deletions

View File

@@ -1,9 +1,9 @@
name: Cypress
on:
pull_request:
branches: [main]
branches: [develop]
push:
branches: [main]
branches: [develop]
jobs:
cypress:
@@ -23,7 +23,7 @@ jobs:
fail-fast: false
matrix:
php-versions: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
ots: ['tfs-1.4', 'canary-3.1.2'] # TODO: add 'tfs-master' (actually doesn't work cause AAC doesn't support reading .env configuration)
ots: ['tfs-1.4', 'canary-3.1.2', 'tfs-0.3'] # TODO: add 'tfs-master' (actually doesn't work cause AAC doesn't support reading .env configuration)
name: Cypress (PHP ${{ matrix.php-versions }}, ${{ matrix.ots }})
steps:
- name: 📌 MySQL Start & init & show db
@@ -58,6 +58,14 @@ jobs:
ref: master
path: ots
- name: Checkout TFS 0.3
uses: actions/checkout@v4
if: matrix.ots == 'tfs-0.3'
with:
repository: otland/tfs-old-svn
ref: 0.3
path: ots
- name: Checkout Canary
uses: actions/checkout@v4
if: matrix.ots == 'canary-3.1.2'
@@ -67,9 +75,15 @@ jobs:
path: ots
- name: Import OTS Schema
if: matrix.ots != 'tfs-0.3'
run: |
mysql -uroot -proot myaac < ots/schema.sql
- name: Import OTS Schema (TFS 0.3)
if: matrix.ots == 'tfs-0.3'
run: |
mysql -uroot -proot myaac < ots/schemas/mysql.sql
- name: Rename config.lua
run: mv ots/config.lua.dist ots/config.lua
@@ -109,6 +123,33 @@ jobs:
regex: false
include: 'ots/config.lua'
- name: Replace mysqlPass (TFS 0.3.6pl1)
uses: jacobtomlinson/gha-find-replace@v3
if: matrix.ots == 'tfs-0.3'
with:
find: 'sqlType = "sqlite"'
replace: 'sqlType = "mysql"'
regex: false
include: 'ots/config.lua'
- name: Replace mysqlPass (TFS 0.3.6pl1)
uses: jacobtomlinson/gha-find-replace@v3
if: matrix.ots == 'tfs-0.3'
with:
find: 'sqlPass = ""'
replace: 'sqlPass = "root"'
regex: false
include: 'ots/config.lua'
- name: Replace mysqlDatabase (Canary)
uses: jacobtomlinson/gha-find-replace@v3
if: matrix.ots == 'tfs-0.3'
with:
find: 'sqlDatabase = "theforgottenserver"'
replace: 'sqlDatabase = "myaac"'
regex: false
include: 'ots/config.lua'
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:

View File

@@ -1,9 +1,9 @@
name: PHP Linting
on:
pull_request:
branches: [main]
branches: [develop]
push:
branches: [main]
branches: [develop]
jobs:
phplint:

View File

@@ -2,9 +2,9 @@ name: "PHPStan"
on:
pull_request:
branches: [main]
branches: [develop]
push:
branches: [main]
branches: [develop]
jobs:
tests:
@@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php-versions: [ '8.1', '8.2', '8.3', '8.4' ]
php-versions: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
steps:
- name: "Checkout"
uses: "actions/checkout@v4"

21
CHANGELOG-2.x.md Normal file
View File

@@ -0,0 +1,21 @@
## [2.0-dev - x.x.2025]
### Added
* Add an "access" option to Menus (#340)
* Possibility to hide menus for unauthorized users
* Add the possibility to fetch skills in the getTopPlayers function (#347)
### Changed
* Better handling of vocations: (#345)
* Load from vocations.xml (No need to manually set)
* 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)
* Admin Panel: save menu collapse state (https://github.com/slawkens/myaac/commit/55da00520df7463a1d1ca41931df1598e9f2ffeb)
### Internal
* Refactor account/lost pages (#326)
* Refactor OTS_Player to support more distros (#348)
* Refactor PHP cache to store expiration and improve typing (https://github.com/slawkens/myaac/commit/96b8e00f4999f8b4c4c97b54b97d91c6fd7df298)
* Move forum show_board code to Twig (https://github.com/slawkens/myaac/commit/e0e0e467012a5fb9979cc4387af6bad1d4540279)
* Save db cache only if it has changed (https://github.com/slawkens/myaac/commit/11cb1cf97e74f3bccf59360e1efb800a426b3d43)

View File

@@ -9,6 +9,7 @@
*/
use MyAAC\Models\Account as AccountModel;
use MyAAC\Models\AccountAction;
use MyAAC\Models\Player;
defined('MYAAC') or die('Direct access not allowed!');
@@ -182,39 +183,7 @@ else if (isset($_REQUEST['search'])) {
$account->setName($name);
}
if ($hasTypeColumn) {
$account->setCustomField('type', $group);
} elseif ($hasGroupColumn) {
$account->setCustomField('group_id', $group);
}
if ($hasSecretColumn) {
$account->setCustomField('secret', $secret);
}
$account->setCustomField('key', $key);
$account->setEMail($email);
if (HAS_ACCOUNT_COINS) {
$account->setCustomField('coins', $t_coins);
}
if (HAS_ACCOUNT_COINS_TRANSFERABLE || HAS_ACCOUNT_TRANSFERABLE_COINS) {
$account->setCustomField(ACCOUNT_COINS_TRANSFERABLE_COLUMN, $t_coins_transferable);
}
$lastDay = 0;
if($p_days != 0 && $p_days != OTS_Account::GRATIS_PREMIUM_DAYS) {
$lastDay = time();
} else if ($lastDay != 0) {
$lastDay = 0;
}
$account->setPremDays($p_days);
$account->setLastLogin($lastDay);
if ($hasPointsColumn) {
$account->setCustomField('premium_points', $p_points);
}
$account->setRLName($rl_name);
$account->setLocation($rl_loca);
@@ -222,9 +191,18 @@ else if (isset($_REQUEST['search'])) {
$account->setCountry($rl_country);
}
$account->setCustomField('created', $created);
$account->setWebFlags($web_flags);
$account->setCustomField('web_lastlogin', $web_lastlogin);
if (!isCanary()) {
$lastDay = 0;
if($p_days != 0 && $p_days != OTS_Account::GRATIS_PREMIUM_DAYS) {
$lastDay = time();
}
$account->setLastLogin($lastDay);
}
$account->setPremDays($p_days);
if (isset($password)) {
if (USE_ACCOUNT_SALT) {
@@ -238,6 +216,34 @@ else if (isset($_REQUEST['search'])) {
}
$account->save();
if ($hasTypeColumn) {
$account->setCustomField('type', $group);
} elseif ($hasGroupColumn) {
$account->setCustomField('group_id', $group);
}
if ($hasSecretColumn) {
$account->setCustomField('secret', $secret);
}
$account->setCustomField('key', $key);
if (HAS_ACCOUNT_COINS) {
$account->setCustomField('coins', $t_coins);
}
if (HAS_ACCOUNT_COINS_TRANSFERABLE || HAS_ACCOUNT_TRANSFERABLE_COINS) {
$account->setCustomField(ACCOUNT_COINS_TRANSFERABLE_COLUMN, $t_coins_transferable);
}
if ($hasPointsColumn) {
$account->setCustomField('premium_points', $p_points);
}
$account->setCustomField('created', $created);
$account->setCustomField('web_lastlogin', $web_lastlogin);
echo_success('Account saved at: ' . date('G:i'));
}
}
@@ -481,9 +487,8 @@ else if (isset($_REQUEST['search'])) {
</thead>
<tbody>
<?php
$accountActions = \MyAAC\Models\AccountAction::where('account_id', $account->getId())->orderByDesc('date')->get();
$accountActions = AccountAction::where('account_id', $account->getId())->orderByDesc('date')->get();
foreach ($accountActions as $i => $log):
$log->ip = ($log->ip != 0 ? long2ip($log->ip) : inet_ntop($log->ipv6));
?>
<tr>
<td><?php echo $i + 1; ?></td>
@@ -631,6 +636,7 @@ else if (isset($_REQUEST['search'])) {
</div>
</form>
</div>
<?php if (USE_ACCOUNT_NAME): ?>
<div class="col-6 col-lg-12">
<form action="<?php echo $admin_base; ?>" method="post">
<?php csrf(); ?>
@@ -641,6 +647,7 @@ else if (isset($_REQUEST['search'])) {
</div>
</form>
</div>
<?php endif; ?>
<div class="col-6 col-lg-12">
<form action="<?php echo $admin_base; ?>" method="post">
<?php csrf(); ?>

View File

@@ -23,6 +23,7 @@ if (!hasFlag(FLAG_CONTENT_MENUS) && !superAdmin()) {
}
$pluginThemes = Plugins::getThemes();
$groups = new OTS_Groups_List();
if (isset($_POST['template'])) {
$template = $_POST['template'];
@@ -32,6 +33,8 @@ if (isset($_POST['template'])) {
$post_menu_link = $_POST['menu_link'] ?? [];
$post_menu_blank = $_POST['menu_blank'] ?? [];
$post_menu_color = $_POST['menu_color'] ?? [];
$post_menu_access = $_POST['menu_access'] ?? [];
if (count($post_menu) != count($post_menu_link)) {
echo 'Menu count is not equal menu links. Something went wrong when sending form.';
return;
@@ -50,6 +53,7 @@ if (isset($_POST['template'])) {
'link' => $post_menu_link[$category][$i],
'blank' => $post_menu_blank[$category][$i] == 'on' ? 1 : 0,
'color' => str_replace('#', '', $post_menu_color[$category][$i]),
'access' => $post_menu_access[$category][$i],
'category' => $category,
'ordering' => $i
]);
@@ -122,7 +126,7 @@ if (isset($_POST['template'])) {
?>
<?php
$menus = Menu::query()
->select('name', 'link', 'blank', 'color', 'category', 'ordering')
->select('name', 'link', 'access', 'blank', 'color', 'category', 'ordering')
->where('enabled', 1)
->where('template', $template)
->orderBy('ordering')
@@ -151,11 +155,34 @@ if (isset($_POST['template'])) {
foreach ($menus[$id] as $menu):
$color = (empty($menu['color']) ? ($cat['default_links_color'] ?? ($config['menu_default_links_color'] ?? ($config['menu_default_color'] ?? '#ffffff'))) : '#' . $menu['color']);
?>
<li class="ui-state-default" id="list-<?php echo $id ?>-<?php echo $i ?>"><label>Name:</label> <input type="text" name="menu[<?php echo $id ?>][]" value="<?php echo escapeHtml($menu['name']); ?>"/>
<label>Link:</label> <input type="text" name="menu_link[<?php echo $id ?>][]" value="<?php echo $menu['link'] ?>"/>
<input type="hidden" name="menu_blank[<?php echo $id ?>][]" value="0"/>
<label><input class="blank-checkbox" type="checkbox" <?php echo($menu['blank'] == 1 ? 'checked' : '') ?>/><span title="Open in New Window">New Window</span></label>
<input class="color-picker" type="text" name="menu_color[<?php echo $id ?>][]" value="<?php echo $color; ?>"/>
<li class="ui-state-default" id="list-<?php echo $id ?>-<?php echo $i ?>">
<label class="label_menu_name">Name: <input type="text" name="menu[<?php echo $id ?>][]" class="form-control menu-name" value="<?php echo escapeHtml($menu['name']); ?>"/>
</label>
<label class="label_menu_link">Link: <input type="text" name="menu_link[<?= $id ?>][]" class="form-control menu-link" value="<?php echo $menu['link'] ?>"/>
</label>
<br/>
<div class="menu-options-row">
<label>Access:
<select name="menu_access[<?= $id ?>][]" class="form-control menu-access">
<option value="0" <?= ($menu['access'] == 0 ? 'selected' : ''); ?>>Guest*</option>
<?php foreach ($groups->getGroups() as $group): ?>
<option value="<?= $group->getId(); ?>" <?= ($menu['access'] == $group->getId() ? 'selected' : ''); ?>><?= ucfirst($group->getName()); ?></option>
<?php endforeach; ?>
</select>
</label>
<label>Color: <input class="menu-color" type="color" name="menu_color[<?php echo $id ?>][]" value="<?php echo $color; ?>"/>
</label>
<input type="hidden" name="menu_blank[<?php echo $id ?>][]" class="menu-blank" value="0"/>
<label><input type="checkbox" class="menu-blank-checkbox" <?php echo($menu['blank'] == 1 ? 'checked' : '') ?>/><span title="Open in New Window">New Window</span></label>
</div>
<a class="remove-button" id="remove-button-<?php echo $id ?>-<?php echo $i ?>"><i class="fas fa-trash"></a></i></li>
<?php $i++; $last_id[$id] = $i;
endforeach;

View File

@@ -10,6 +10,7 @@
use MyAAC\Forum;
use MyAAC\Models\Player;
use MyAAC\Server\XML\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
@@ -34,6 +35,7 @@ $skills = array(
$hasBlessingsColumn = $db->hasColumn('players', 'blessings');
$hasBlessingColumn = $db->hasColumn('players', 'blessings1');
$hasLookAddons = $db->hasColumn('players', 'lookaddons');
$hasCapColumn = $db->hasColumn('players', 'cap');
$skull_type = array("None", "Yellow", "Green", "White", "Red", "Black", "Orange");
?>
@@ -166,8 +168,11 @@ else if (isset($_REQUEST['search'])) {
$town = $_POST['town'];
verify_number($town, 'Town', 11);
$capacity = $_POST['capacity'];
verify_number($capacity, 'Capacity', 11);
if ($hasCapColumn) {
$capacity = $_POST['capacity'];
verify_number($capacity, 'Capacity', 11);
}
$sex = $_POST['sex'];
verify_number($sex, 'Sex', 1);
@@ -237,7 +242,30 @@ else if (isset($_REQUEST['search'])) {
$player->setGroup($groups->getGroup($group));
$player->setLevel($level);
$player->setExperience($experience);
if ($db->hasColumn('players', 'promotion')) {
$promotion = 0;
$vocationOriginal = Vocations::getOriginal($vocation);
if ($vocation != $vocationOriginal) {
$tmpId = $vocationOriginal;
while($promoted = Vocations::getPromoted($tmpId)) {
$promotion++;
$tmpId = $promoted;
if ($promoted == $vocation) {
break;
}
}
$vocation = $vocationOriginal;
}
$player->setPromotion($promotion);
}
$player->setVocation($vocation);
$player->setHealth($health);
$player->setHealthMax($health_max);
$player->setMagLevel($magic_level);
@@ -249,16 +277,20 @@ else if (isset($_REQUEST['search'])) {
$player->setLookHead($look_head);
$player->setLookLegs($look_legs);
$player->setLookType($look_type);
if ($hasLookAddons)
if ($hasLookAddons) {
$player->setLookAddons($look_addons);
if ($db->hasColumn('players', 'offlinetraining_time'))
$player->setCustomField('offlinetraining_time', $offlinetraining);
}
$player->setPosX($pos_x);
$player->setPosY($pos_y);
$player->setPosZ($pos_z);
$player->setSoul($soul);
$player->setTownId($town);
$player->setCap($capacity);
if ($hasCapColumn) {
$player->setCap($capacity);
}
$player->setSex($sex);
$player->setLastLogin($lastlogin);
$player->setLastLogout($lastlogout);
@@ -275,23 +307,11 @@ else if (isset($_REQUEST['search'])) {
if ($hasBlessingsColumn)
$player->setBlessings($blessings);
if ($hasBlessingColumn) {
for ($i = 1; $i <= $bless_count; $i++) {
$a = 'blessing' . $i;
$player->setCustomField('blessings' . $i, ${'blessing' . $i} ? '1' : '0');
}
}
$player->setBalance($balance);
if ($db->hasColumn('players', 'stamina'))
$player->setStamina($stamina);
if ($db->hasColumn('players', 'deletion'))
$player->setCustomField('deletion', $deleted ? '1' : '0');
else
$player->setCustomField('deleted', $deleted ? '1' : '0');
$player->setCustomField('hide', $hide ? '1' : '0');
$player->setCustomField('created', $created);
if (isset($comment))
$player->setCustomField('comment', $comment);
$player->setDeleted($deleted ? '1' : '0');
foreach ($_POST['skills'] as $skill => $value) {
$player->setSkill($skill, $value);
@@ -300,6 +320,24 @@ else if (isset($_REQUEST['search'])) {
$player->setSkillTries($skill, $value);
}
$player->save();
if ($db->hasColumn('players', 'offlinetraining_time')) {
$player->setCustomField('offlinetraining_time', $offlinetraining);
}
if ($hasBlessingColumn) {
for ($i = 1; $i <= $bless_count; $i++) {
$a = 'blessing' . $i;
$player->setCustomField('blessings' . $i, ${'blessing' . $i} ? '1' : '0');
}
}
$player->setCustomField('hide', $hide ? '1' : '0');
$player->setCustomField('created', $created);
if (isset($comment)) {
$player->setCustomField('comment', $comment);
}
echo_success('Player saved at: ' . date('G:i'));
$player->load($id);
}
@@ -531,10 +569,12 @@ else if (isset($_REQUEST['search'])) {
</div>
</div>
<div class="form-group row">
<?php if($hasCapColumn): ?>
<div class="col-12 col-sm-12 col-lg-6">
<label for="capacity" class="control-label">Capacity:</label>
<input type="text" class="form-control" id="capacity" name="capacity" autocomplete="off" size="3" maxlength="11" value="<?php echo $player->getCap(); ?>"/>
</div>
<?php endif; ?>
<div class="col-12 col-sm-12 col-lg-6">
<label for="soul" class="control-label">Soul:</label>
<input type="text" class="form-control" id="soul" name="soul" autocomplete="off" size="3" maxlength="10" value="<?php echo $player->getSoul(); ?>"/>

View File

@@ -19,14 +19,14 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
<?php $hooks->trigger(HOOK_ADMIN_HEAD_END); ?>
</head>
<body class="sidebar-mini ">
<body class="sidebar-mini <?= (session('admin.menu-collapse') ? 'sidebar-collapse' : ''); ?>">
<?php $hooks->trigger(HOOK_ADMIN_BODY_START); ?>
<?php if ($logged && admin()) { ?>
<div class="wrapper">
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#"><i class="fas fa-bars"></i></a>
<a class="nav-link sidebar-toggle" data-widget="pushmenu" href="#"><i class="fas fa-bars"></i></a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="<?php echo ADMIN_URL; ?>" class="nav-link">Home</a>
@@ -198,6 +198,7 @@ if ($logged && admin()) {
<script src="<?php echo BASE_URL; ?>tools/js/datatables.bs.min.js"></script>
<?php } ?>
<script src="<?php echo BASE_URL; ?>tools/js/adminlte.min.js"></script>
<?php $twig->display('admin.menu-collapse.html.twig'); ?>
<?php $hooks->trigger(HOOK_ADMIN_BODY_END); ?>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<?php
const MYAAC_ADMIN = true;
const IGNORE_SET_LAST_VISIT = true;
require '../../common.php';
require SYSTEM . 'functions.php';
require SYSTEM . 'init.php';
require SYSTEM . 'login.php';
if(!admin()) {
http_response_code(500);
die('You are not logged in. Probably session expired. Please login again.');
}
if (!isset($_POST['collapse'])) {
http_response_code(500);
die('Something went wrong.');
}
csrfProtect();
setSession('admin.menu-collapse', $_POST['collapse'] == 'true');

View File

@@ -26,8 +26,8 @@
if (version_compare(phpversion(), '8.1', '<')) die('PHP version 8.1 or higher is required.');
const MYAAC = true;
const MYAAC_VERSION = '1.8.8';
const DATABASE_VERSION = 46;
const MYAAC_VERSION = '2.0-dev';
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'));

View File

@@ -19,7 +19,8 @@
"symfony/var-dumper": "^6.4",
"filp/whoops": "^2.15",
"maximebf/debugbar": "1.*",
"guzzlehttp/guzzle": "7.9.3"
"guzzlehttp/guzzle": "7.9.3",
"spomky-labs/otphp": "^11.3"
},
"require-dev": {
"phpstan/phpstan": "^1.10"

717
composer.lock generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/monk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -3,6 +3,7 @@ defined('MYAAC') or die('Direct access not allowed!');
use MyAAC\Models\Changelog;
use MyAAC\Models\Config;
use MyAAC\Models\FAQ;
use MyAAC\Models\ForumBoard;
use MyAAC\Models\Gallery;
use MyAAC\Models\NewsCategory;
@@ -56,13 +57,10 @@ if (NewsCategory::count() === 0) {
}
}
if (Gallery::count() === 0) {
Gallery::create([
'comment' => 'Demon',
'image' => 'images/gallery/demon.jpg',
'thumb' => 'images/gallery/demon_thumb.gif',
'author' => 'MyAAC',
'ordering' => 0,
if(FAQ::count() == 0) {
FAQ::create([
'question' => 'What is this?',
'answer' => 'This is website for OTS powered by MyAAC.',
]);
}

View File

@@ -1,11 +1,20 @@
CREATE TABLE IF NOT EXISTS `myaac_account_actions`
(
`id` int NOT NULL AUTO_INCREMENT,
`account_id` int NOT NULL,
`ip` int unsigned NOT NULL DEFAULT 0,
`ipv6` binary(16) NOT NULL DEFAULT 0,
`ip` varchar(45) NOT NULL DEFAULT '',
`date` int NOT NULL DEFAULT 0,
`action` varchar(255) NOT NULL DEFAULT '',
KEY (`account_id`)
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`
@@ -102,6 +111,7 @@ CREATE TABLE IF NOT EXISTS `myaac_menu`
`template` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`link` varchar(255) NOT NULL,
`access` tinyint NOT NULL DEFAULT 0,
`blank` tinyint NOT NULL DEFAULT 0,
`color` varchar(6) NOT NULL DEFAULT '',
`category` int NOT NULL DEFAULT 1,
@@ -197,18 +207,6 @@ CREATE TABLE IF NOT EXISTS `myaac_pages`
UNIQUE (`name`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `myaac_gallery`
(
`id` int NOT NULL AUTO_INCREMENT,
`comment` varchar(255) NOT NULL DEFAULT '',
`image` varchar(255) NOT NULL,
`thumb` varchar(255) NOT NULL,
`author` varchar(50) NOT NULL DEFAULT '',
`ordering` int NOT NULL DEFAULT 0,
`hide` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `myaac_settings`
(
`id` int NOT NULL AUTO_INCREMENT,

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

@@ -2,8 +2,6 @@
define('MYAAC_INSTALL', true);
use MyAAC\DataLoader;
use MyAAC\Models\FAQ as ModelsFAQ;
use MyAAC\Plugins;
require_once '../../common.php';
@@ -25,34 +23,9 @@ if(isset($config['installed']) && $config['installed'] && !isset($_SESSION['save
require SYSTEM . 'init.php';
if ($db->hasTable('players')) {
$deleted = 'deleted';
if ($db->hasColumn('players', 'deletion'))
$deleted = 'deletion';
$time = time();
function insert_sample_if_not_exist($p)
{
global $db, $success, $deleted, $time;
$query = $db->query('SELECT `id` FROM `players` WHERE `name` = ' . $db->quote($p['name']));
if ($query->rowCount() == 0) {
if (!query("INSERT INTO `players` (`id`, `name`, `group_id`, `account_id`, `level`, `vocation`, `health`, `healthmax`, `experience`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `maglevel`, `mana`, `manamax`, `manaspent`, `soul`, `town_id`, `posx`, `posy`, `posz`, `conditions`, `cap`, `sex`, `lastlogin`, `lastip`, `save`, `lastlogout`, `balance`, `$deleted`, `created`, `hide`, `comment`) VALUES (null, " . $db->quote($p['name']) . ", 1, " . getSession('account') . ", " . $p['level'] . ", " . $p['vocation_id'] . ", " . $p['health'] . ", " . $p['healthmax'] . ", " . $p['experience'] . ", 118, 114, 38, 57, " . $p['looktype'] . ", 0, " . $p['mana'] . ", " . $p['manamax'] . ", 0, " . $p['soul'] . ", 1, 1000, 1000, 7, '', " . $p['cap'] . ", 1, " . $time . ", 2130706433, 1, " . $time . ", 0, 0, " . $time . ", 1, '');"))
$success = false;
}
}
$success = true;
insert_sample_if_not_exist(array('name' => 'Rook Sample', 'level' => 1, 'vocation_id' => 0, 'health' => 150, 'healthmax' => 150, 'experience' => 0, 'looktype' => 130, 'mana' => 0, 'manamax' => 0, 'soul' => 100, 'cap' => 400));
insert_sample_if_not_exist(array('name' => 'Sorcerer Sample', 'level' => 8, 'vocation_id' => 1, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 130, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470));
insert_sample_if_not_exist(array('name' => 'Druid Sample', 'level' => 8, 'vocation_id' => 2, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 130, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470));
insert_sample_if_not_exist(array('name' => 'Paladin Sample', 'level' => 8, 'vocation_id' => 3, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 129, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470));
insert_sample_if_not_exist(array('name' => 'Knight Sample', 'level' => 8, 'vocation_id' => 4, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 131, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470));
if ($success) {
success($locale['step_database_imported_players']);
}
}
// add player samples
require_once SYSTEM . 'migrations/49.php';
$up();
DataLoader::setLocale($locale);
DataLoader::load();
@@ -61,10 +34,6 @@ DataLoader::load();
require_once SYSTEM . 'migrations/17.php';
$up();
// update config.highscores_ids_hidden
require_once SYSTEM . 'migrations/20.php';
$up();
// add z_polls tables
require_once SYSTEM . 'migrations/22.php';
$up();
@@ -83,13 +52,6 @@ $up();
require_once SYSTEM . 'migrations/45.php';
$up();
if(ModelsFAQ::count() == 0) {
ModelsFAQ::create([
'question' => 'What is this?',
'answer' => 'This is website for OTS powered by MyAAC.',
]);
}
$hooks->trigger(HOOK_INSTALL_FINISH);
$db->setClearCacheAfter(true);

104
login.php
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';
@@ -12,7 +13,7 @@ require_once SYSTEM . 'init.php';
require_once SYSTEM . 'status.php';
# error function
function sendError($message, $code = 3){
function sendError($message, $code = 3) {
$ret = [];
$ret['errorCode'] = $code;
$ret['errorMessage'] = $message;
@@ -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);
}
sendError('Submit a valid two-factor authentication token.', 6);
} else {
require_once LIBS . 'rfc6238.php';
if (TokenAuth6238::verify($accountSecret, $inputToken) !== true) {
$limiter->increment($ip);
if ($limiter->exceeded($ip)) {
sendError($ban_msg);
}
$twoFactorAuth = TwoFactorAuth::getInstance($account->id);
sendError('Two-factor authentication failed, token is wrong.', 6);
}
}
$code = '';
if ($twoFactorAuth->isActive()) {
if ($twoFactorAuth->getAuthType() === TwoFactorAuth::TYPE_EMAIL) {
$code = $request->emailcode ?? false;
}
else if ($twoFactorAuth->getAuthType() === TwoFactorAuth::TYPE_APP) {
$code = $request->token ?? false;
}
}
$error = '';
$errorCode = 6;
if (!$twoFactorAuth->processClientLogin($code, $error, $errorCode)) {
$limiter->increment($ip);
if ($limiter->exceeded($ip)) {
sendError($ban_msg);
}
sendError($error, $errorCode);
}
$limiter->reset($ip);
@@ -220,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

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

@@ -5,8 +5,6 @@ $deprecatedConfig = [
'genders',
'template',
'template_allow_change',
'vocations_amount',
'vocations',
'client',
'session_prefix',
'friendly_urls',

View File

@@ -17,6 +17,8 @@ use MyAAC\Models\Guild;
use MyAAC\Models\House;
use MyAAC\Models\Pages;
use MyAAC\Models\Player;
use MyAAC\Models\PlayerDeath;
use MyAAC\Models\PlayerKillers;
use MyAAC\News;
use MyAAC\Plugins;
use MyAAC\Settings;
@@ -515,7 +517,12 @@ function template_place_holder($type): string
$ret .= $debugBarRenderer->renderHead();
}
}
elseif ($type === 'head_end') {
$ret .= setting('core.html_head');
}
elseif ($type === 'body_start') {
$ret .= setting('core.html_body');
$ret .= $twig->render('browsehappy.html.twig');
if (admin()) {
@@ -526,6 +533,8 @@ function template_place_holder($type): string
}
}
elseif($type === 'body_end') {
$ret .= setting('core.html_footer');
$ret .= template_ga_code();
if (isset($debugBar)) {
$ret .= $debugBarRenderer->render();
@@ -1132,11 +1141,44 @@ function csrfProtect(): void
}
}
function getTopPlayers($limit = 5, $skill = 'level') {
function getSkillIdByName(string $name): int|null
{
$skills = [
'level' => POT::SKILL_LEVEL,
'experience' => POT::SKILL_LEVEL,
'magic' => POT::SKILL_MAGIC,
'maglevel' => POT::SKILL_MAGIC,
'balance' => SKILL_BALANCE,
'frags' => SKILL_FRAGS,
'club' => POT::SKILL_CLUB,
'sword' => POT::SKILL_SWORD,
'axe' => POT::SKILL_AXE,
'dist' => POT::SKILL_DIST,
'distance' => POT::SKILL_DIST,
'shield' => POT::SKILL_SHIELD,
'shielding' => POT::SKILL_SHIELD,
'fish' => POT::SKILL_FISH,
'fishing' => POT::SKILL_FISH,
];
return $skills[$name] ?? null;
}
function getTopPlayers($limit = 5, $skill = POT::SKILL_LEVEL)
{
global $db;
if ($skill === 'level') {
$skill = 'experience';
$skillOriginal = $skill;
if (is_string($skill)) {
$skill = getSkillIdByName($skill);
}
if (!is_numeric($skill)) {
throw new RuntimeException("getTopPlayers: Invalid skill: $skillOriginal");
}
return Cache::remember("top_{$limit}_{$skill}", 2 * 60, function () use ($db, $limit, $skill) {
@@ -1157,15 +1199,64 @@ function getTopPlayers($limit = 5, $skill = 'level') {
$columns[] = 'lookmount';
}
return Player::query()
$query = Player::query()
->select($columns)
->withOnlineStatus()
->notDeleted()
->where('group_id', '<', setting('core.highscores_groups_hidden'))
->whereNotIn('id', setting('core.highscores_ids_hidden'))
->where('account_id', '!=', 1)
->orderByDesc($skill)
->limit($limit)
->orderByDesc('value');
if ($limit > 0) {
$query->limit($limit);
}
if ($skill >= POT::SKILL_FIRST && $skill <= POT::SKILL_LAST) { // skills
if ($db->hasColumn('players', 'skill_fist')) {// tfs 1.0
$skill_ids = array(
POT::SKILL_FIST => 'skill_fist',
POT::SKILL_CLUB => 'skill_club',
POT::SKILL_SWORD => 'skill_sword',
POT::SKILL_AXE => 'skill_axe',
POT::SKILL_DIST => 'skill_dist',
POT::SKILL_SHIELD => 'skill_shielding',
POT::SKILL_FISH => 'skill_fishing',
);
$query
->addSelect($skill_ids[$skill] . ' as value')
->orderByDesc($skill_ids[$skill] . '_tries');
} else {
$query
->join('player_skills', 'player_skills.player_id', '=', 'players.id')
->where('skillid', $skill)
->addSelect('player_skills.value as value');
}
} else if ($skill == SKILL_FRAGS) // frags
{
if ($db->hasTable('player_killers')) {
$query->addSelect(['value' => PlayerKillers::whereColumn('player_killers.player_id', 'players.id')->selectRaw('COUNT(*)')]);
} else {
$query->addSelect(['value' => PlayerDeath::unjustified()->whereColumn('player_deaths.killed_by', 'players.name')->selectRaw('COUNT(*)')]);
}
} else if ($skill == SKILL_BALANCE) // balance
{
$query
->addSelect('players.balance as value');
} else {
if ($skill == POT::SKILL_MAGIC) {
$query
->addSelect('players.maglevel as value', 'players.maglevel')
->orderByDesc('manaspent');
} else { // level
$query
->addSelect('players.level as value', 'players.experience')
->orderByDesc('experience');
}
}
return $query
->get()
->map(function ($e, $i) {
$row = $e->toArray();
@@ -1717,8 +1808,8 @@ function getAccountIdentityColumn(): string
function isCanary(): bool
{
$vipSystemEnabled = configLua('vipSystemEnabled');
return isset($vipSystemEnabled);
$dataPackDirectory = configLua('dataPackDirectory');
return isset($dataPackDirectory);
}
function getStatusUptimeReadable(int $uptime): string

View File

@@ -14,6 +14,7 @@ use MyAAC\CsrfToken;
use MyAAC\Hooks;
use MyAAC\Plugins;
use MyAAC\Models\Town;
use MyAAC\Server\XML\Vocations;
use MyAAC\Settings;
defined('MYAAC') or die('Direct access not allowed!');
@@ -214,3 +215,5 @@ if (count($towns) <= 0) {
config(['towns', $towns]);
unset($towns);
new Vocations();

View File

@@ -12,6 +12,9 @@
* @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU Lesser General Public License, Version 3
*/
use MyAAC\Models\Account as AccountModel;
use MyAAC\Models\AccountAction;
/**
* OTServ account abstraction.
*
@@ -40,7 +43,11 @@ class OTS_Account extends OTS_Row_DAO implements IteratorAggregate, Countable
*/
private $data = array('email' => '', 'rlname' => '','location' => '', 'country' => '','web_flags' => 0, 'lastday' => 0, 'premdays' => 0, 'created' => 0);
public static $cache = array();
private array $columns = ['password', 'email', 'rlname', 'location', 'country', 'web_flags', 'created'];
private array $optionalColumns = ['name', 'number', 'lastday', 'premdays', 'premium_ends_at', 'premend'];
public static array $cache = [];
const GRATIS_PREMIUM_DAYS = 65535;
/**
@@ -325,27 +332,50 @@ class OTS_Account extends OTS_Row_DAO implements IteratorAggregate, Countable
*/
public function save()
{
if( !isset($this->data['id']) )
{
if (!isset($this->data['id'])) {
throw new E_OTS_NotLoaded();
}
$field = 'lastday';
if($this->db->hasColumn('accounts', 'premend')) { // othire
$field = 'premend';
if(!isset($this->data['premend'])) {
$this->data['premend'] = 0;
}
}
else if($this->db->hasColumn('accounts', 'premium_ends_at')) {
$field = 'premium_ends_at';
if(!isset($this->data['premium_ends_at'])) {
$this->data['premium_ends_at'] = 0;
}
}
$defaultValues = [
'premium_ends_at' => 0,
'lastday' => 0,
'premend' => 0,
'premdays' => 0,
];
// UPDATE query on database
$this->db->exec('UPDATE `accounts` SET ' . ($this->db->hasColumn('accounts', 'name') ? '`name` = ' . $this->db->quote($this->data['name']) . ',' : '') . '`password` = ' . $this->db->quote($this->data['password']) . ', `email` = ' . $this->db->quote($this->data['email']) . ', `rlname` = ' . $this->db->quote($this->data['rlname']) . ', `location` = ' . $this->db->quote($this->data['location']) . ', `country` = ' . $this->db->quote($this->data['country']) . ', `web_flags` = ' . (int) $this->data['web_flags'] . ', ' . ($this->db->hasColumn('accounts', 'premdays') ? '`premdays` = ' . (int) $this->data['premdays'] . ',' : '') . '`' . $field . '` = ' . (int) $this->data[$field] . ' WHERE `id` = ' . $this->data['id']);
foreach ($defaultValues as $key => $value) {
if (!isset($this->data[$key])) {
$this->data[$key] = $value;
}
}
$columns = $this->columns;
foreach ($this->optionalColumns as $column) {
if ($this->db->hasColumn('accounts', $column)) {
$columns[] = $column;
}
}
$values = [];
foreach ($columns as $column) {
$value = $this->data[$column];
$values[$column] = $value;
}
// updates existing player
if( isset($this->data['id']) ) {
AccountModel::where('id', $this->data['id'])->update($values);
}
// creates new player
else {
$values['created'] = time();
$account = AccountModel::create($values);
// ID of new player
$this->data['id'] = $account->id;
}
}
/**
@@ -504,11 +534,17 @@ class OTS_Account extends OTS_Row_DAO implements IteratorAggregate, Countable
* @since 0.7.5
* @throws E_OTS_NotLoaded If account is not loaded.
*/
public function setPremDays($premdays)
public function setPremDays($premdays): void
{
$this->data['premdays'] = (int) $premdays;
$this->data['premend'] = time() + ($premdays * 24 * 60 * 60);
$this->data['premium_ends_at'] = time() + ($premdays * 24 * 60 * 60);
$premiumTimeInSeconds = time() + ($premdays * 24 * 60 * 60);
$this->data['premend'] = $premiumTimeInSeconds;
$this->data['premium_ends_at'] = $premiumTimeInSeconds;
if (isCanary()) {
$this->data['lastday'] = $premiumTimeInSeconds;
}
}
public function setRLName($name)
@@ -700,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]);
}
/**
@@ -1007,26 +1037,16 @@ class OTS_Account extends OTS_Row_DAO implements IteratorAggregate, Countable
public function logAction($action)
{
$ip = get_browser_real_ip();
if(!str_contains($ip, ":")) {
$ipv6 = '0';
}
else {
$ipv6 = $ip;
$ip = '';
}
return $this->db->exec('INSERT INTO `' . TABLE_PREFIX . 'account_actions` (`account_id`, `ip`, `ipv6`, `date`, `action`) VALUES (' . $this->db->quote($this->getId()).', ' . ($ip == '' ? '0' : $this->db->quote(ip2long($ip))) . ', (' . ($ipv6 == '0' ? $this->db->quote('') : $this->db->quote(inet_pton($ipv6))) . '), UNIX_TIMESTAMP(NOW()), ' . $this->db->quote($action).')');
AccountAction::create([
'account_id' => $this->getId(),
'ip' => get_browser_real_ip(),
'date' => time(),
'action' => $action,
]);
}
public function getActionsLog($limit1, $limit2)
{
$actions = array();
foreach($this->db->query('SELECT `ip`, `ipv6`, `date`, `action` FROM `' . TABLE_PREFIX . 'account_actions` WHERE `account_id` = ' . $this->data['id'] . ' ORDER by `date` DESC LIMIT ' . $limit1 . ', ' . $limit2 . '')->fetchAll() as $a)
$actions[] = array('ip' => $a['ip'], 'ipv6' => $a['ipv6'], 'date' => $a['date'], 'action' => $a['action']);
return $actions;
public function getActionsLog($limit) {
return AccountAction::where('account_id', $this->data['id'])->orderByDesc('date')->limit($limit)->get()->toArray();
}
/**
* Returns players iterator.

View File

@@ -26,6 +26,7 @@ use MyAAC\Cache\Cache;
*/
class OTS_DB_MySQL extends OTS_Base_DB
{
private bool $hasCacheChanged = false;
private array $has_table_cache = [];
private array $has_column_cache = [];
private array $get_column_info_cache = [];
@@ -164,7 +165,7 @@ class OTS_DB_MySQL extends OTS_Base_DB
$cache->delete('database_columns_info');
$cache->delete('database_checksum');
}
else {
else if ($this->hasCacheChanged) {
$cache->set('database_tables', serialize($this->has_table_cache), 3600);
$cache->set('database_columns', serialize($this->has_column_cache), 3600);
$cache->set('database_columns_info', serialize($this->get_column_info_cache), 3600);
@@ -228,6 +229,8 @@ class OTS_DB_MySQL extends OTS_Base_DB
private function hasTableInternal($name): bool
{
$this->hasCacheChanged = true;
return ($this->has_table_cache[$name] = $this->query('SELECT `TABLE_NAME` FROM `information_schema`.`tables` WHERE `TABLE_SCHEMA` = ' . $this->quote(config('database_name')) . ' AND `TABLE_NAME` = ' . $this->quote($name) . ' LIMIT 1;')->rowCount() > 0);
}
@@ -241,6 +244,8 @@ class OTS_DB_MySQL extends OTS_Base_DB
}
private function hasColumnInternal($table, $column): bool {
$this->hasCacheChanged = true;
return $this->hasTable($table) && ($this->has_column_cache[$table . '.' . $column] = count($this->query('SHOW COLUMNS FROM `' . $table . "` LIKE " . $this->quote($column))->fetchAll()) > 0);
}
@@ -272,11 +277,14 @@ class OTS_DB_MySQL extends OTS_Base_DB
return false;
}
$this->hasCacheChanged = true;
$formatResult = function ($result) {
return [
'field' => $result['Field'],
'type' => $result['Type'],
'null' => strtolower($result['Null']),
'key' => strtolower($result['Key'] ?? ''),
'default' => $result['Default'],
'extra' => $result['Extra'],
];

View File

@@ -1,20 +1,6 @@
<?php
$__load = array();
/*
'loss_experience' => NULL,
'loss_items' => NULL,
'guild_info' => NULL,
'skull_type' => NULL,
'skull_time' => NULL,
'blessings' => NULL,
'direction' => NULL,
'stamina' => NULL,
'world_id' => NULL,
'online' => NULL,
'deletion' => NULL,
'promotion' => NULL,
'marriage' => NULL
);*/
use MyAAC\Models\Player as PlayerModel;
/**#@+
* @version 0.0.1
@@ -109,6 +95,10 @@ class OTS_Player extends OTS_Row_DAO
POT::SKILL_FISH => array('value' => 0, 'tries' => 0)
);
private array $columns = ['name', 'account_id', 'group_id', 'sex', 'vocation', 'experience', 'level', 'maglevel', 'health', 'healthmax', 'mana', 'manamax', 'manaspent', 'soul', 'lookbody', 'lookfeet', 'lookhead', 'looklegs', 'looktype', 'posx', 'posy', 'posz', 'lastlogin', 'lastlogout', 'lastip', 'town_id', 'balance', 'created', 'comment', 'hide'];
private array $optionalColumns = ['cap', 'skull', 'skull_type', 'skull_time', 'loss_experience', 'loss_mana', 'loss_skills', 'loss_items', 'loss_containers', 'guildnick', 'rank_id', 'promotion', 'direction', 'blessings', 'stamina', 'lookaddons', 'save', 'conditions', 'world_id', 'online', 'deletion', 'deleted', 'marriage'];
private static array $playersOnline;
/**
* Magic PHP5 method.
@@ -133,90 +123,14 @@ class OTS_Player extends OTS_Row_DAO
*/
public function load($id, $fields = null, $load_skills = true)
{
global $__load;
if(!isset($__load['loss_experience']))
{
$loss = '';
if($this->db->hasColumn('players', 'loss_experience')) {
$loss = ', `loss_experience`, `loss_mana`, `loss_skills`';
$columns = $this->columns;
foreach ($this->optionalColumns as $column) {
if ($this->db->hasColumn('players', $column)) {
$columns[] = $column;
}
$__load['loss_experience'] = $loss;
}
if(!isset($__load['loss_items']))
{
$loss_items = '';
if($this->db->hasColumn('players', 'loss_items')) {
$loss_items = ', `loss_items`, `loss_containers`';
}
$__load['loss_items'] = $loss_items;
}
if(!isset($__load['guild_info']))
{
$guild_info = '';
if(!$this->db->hasTable('guild_members') && $this->db->hasColumn('players', 'guildnick')) {
$guild_info = ', `guildnick`, `rank_id`';
}
$__load['guild_info'] = $guild_info;
}
if(!isset($__load['skull_type']))
{
$skull_type = 'skull';
if($this->db->hasColumn('players', 'skull_type')) {
$skull_type = 'skull_type';
}
$__load['skull_type'] = $skull_type;
}
if(!isset($__load['skull_time']))
{
$skull_time = 'skulltime';
if($this->db->hasColumn('players', 'skull_time')) {
$skull_time = 'skull_time';
}
$__load['skull_time'] = $skull_time;
}
if(!isset($__load['blessings'])) {
$__load['blessings'] = $this->db->hasColumn('players', 'blessings');
}
if(!isset($__load['direction'])) {
$__load['direction'] = $this->db->hasColumn('players', 'direction');
}
if(!isset($__load['stamina'])) {
$__load['stamina'] = $this->db->hasColumn('players', 'stamina');
}
if(!isset($__load['world_id'])) {
$__load['world_id'] = $this->db->hasColumn('players', 'world_id');
}
if(!isset($__load['online'])) {
$__load['online'] = $this->db->hasColumn('players', 'online');
}
if(!isset($__load['deletion'])) {
$__load['deletion'] = $this->db->hasColumn('players', 'deletion');
}
if(!isset($__load['promotion'])) {
$__load['promotion'] = $this->db->hasColumn('players', 'promotion');
}
if(!isset($__load['marriage'])) {
$__load['marriage'] = $this->db->hasColumn('players', 'marriage');
}
if(isset($fields)) { // load only what we wish
if(in_array('promotion', $fields)) {
if(!$this->db->hasColumn('players', 'promotion')) {
unset($fields[array_search('promotion', $fields)]);
}
}
if(in_array('deleted', $fields)) {
if($this->db->hasColumn('players', 'deletion')) {
unset($fields[array_search('deleted', $fields)]);
@@ -224,21 +138,21 @@ class OTS_Player extends OTS_Row_DAO
}
}
if(in_array('online', $fields)) {
if(!$this->db->hasColumn('players', 'online')) {
unset($fields[array_search('online', $fields)]);
$columns = [];
foreach ($fields as $field) {
if ($this->db->hasColumn('players', $field)) {
$columns[] = $field;
}
}
$this->data = $this->db->query('SELECT ' . implode(', ', $fields) . ' FROM `players` WHERE `id` = ' . (int)$id)->fetch();
}
else {
// SELECT query on database
$this->data = $this->db->query('SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`' . ($this->db->hasColumn('players', 'lookaddons') ? ', `lookaddons`' : '') . ', `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `save`, `conditions`, `' . $__load['skull_time'] . '` as `skulltime`, `' . $__load['skull_type'] . '` as `skull`' . $__load['guild_info'] . ', `town_id`' . $__load['loss_experience'] . $__load['loss_items'] . ', `balance`' . ($__load['blessings'] ? ', `blessings`' : '') . ($__load['direction'] ? ', `direction`' : '') . ($__load['stamina'] ? ', `stamina`' : '') . ($__load['world_id'] ? ', `world_id`' : '') . ($__load['online'] ? ', `online`' : '') . ', `' . ($__load['deletion'] ? 'deletion' : 'deleted') . '`' . ($__load['promotion'] ? ', `promotion`' : '') . ($__load['marriage'] ? ', `marriage`' : '') . ', `comment`, `created`, `hide` FROM `players` WHERE `id` = ' . (int)$id)->fetch();
}
array_unshift($columns, 'id');
$query = PlayerModel::where('id', $id)->first($columns);
$this->data = $query ? $query->toArray() : [];
// loads skills
if( $this->isLoaded() && $load_skills)
{
if( $this->isLoaded() && $load_skills) {
if($this->db->hasColumn('players', 'skill_fist')) {
$skill_ids = array(
@@ -318,153 +232,65 @@ class OTS_Player extends OTS_Row_DAO
*/
public function save()
{
$skull_type = 'skull';
if($this->db->hasColumn('players', 'skull_type')) {
$skull_type = 'skull_type';
$defaultValues = [
'cap' => 0,
'skull' => 0,
'skull_type' => 0,
'skull_time' => 0,
'loss_experience' => 100,
'loss_mana' => 100,
'loss_skills' => 100,
'loss_items' => 100,
'loss_containers' => 100,
'guildnick' => '',
'rank_id' => 0,
'promotion' => 0,
'direction' => 0,
'blessings' => 0,
'stamina' => 0,
'lookaddons' => 0,
'save' => 1,
'conditions' => '',
'town_id' => 1,
'world_id' => 1,
'online' => 0,
'deletion' => 0,
'deleted' => 0,
'marriage' => 0,
];
foreach ($defaultValues as $key => $value) {
if (!isset($this->data[$key])) {
$this->data[$key] = $value;
}
}
$skull_time = 'skulltime';
if($this->db->hasColumn('players', 'skull_time')) {
$skull_time = 'skull_time';
$columns = $this->columns;
foreach ($this->optionalColumns as $column) {
if ($this->db->hasColumn('players', $column)) {
$columns[] = $column;
}
}
if(!isset($this->data['loss_experience']))
$this->data['loss_experience'] = 100;
$values = [];
foreach ($columns as $column) {
$value = $this->data[$column];
if(!isset($this->data['loss_mana']))
$this->data['loss_mana'] = 100;
if(!isset($this->data['loss_skills']))
$this->data['loss_skills'] = 100;
if(!isset($this->data['loss_items']))
$this->data['loss_items'] = 10;
if(!isset($this->data['loss_containers']))
$this->data['loss_containers'] = 100;
if(!isset($this->data['guildnick']))
$this->data['guildnick'] = '';
if(!isset($this->data['rank_id']))
$this->data['rank_id'] = 0;
if(!isset($this->data['promotion']))
$this->data['promotion'] = 0;
if(!isset($this->data['direction']))
$this->data['direction'] = 0;
if(!isset($this->data['conditions']))
$this->data['conditions'] = '';
if(!isset($this->data['town_id']))
$this->data['town_id'] = 1;
$values[$column] = $value;
}
// updates existing player
if( isset($this->data['id']) )
{
$loss = '';
if($this->db->hasColumn('players', 'loss_experience')) {
$loss = ', `loss_experience` = ' . $this->data['loss_experience'] . ', `loss_mana` = ' . $this->data['loss_mana'] . ', `loss_skills` = ' . $this->data['loss_skills'];
}
$loss_items = '';
if($this->db->hasColumn('players', 'loss_items')) {
$loss_items = ', `loss_items` = ' . $this->data['loss_items'] . ', `loss_containers` = ' . $this->data['loss_containers'];
}
$guild_info = '';
if(!$this->db->hasTable('guild_members') && $this->db->hasColumn('players', 'guildnick')) {
$guild_info = ', `guildnick` = ' . $this->db->quote($this->data['guildnick']) . ', ' . $this->db->fieldName('rank_id') . ' = ' . $this->data['rank_id'];
}
$direction = '';
if($this->db->hasColumn('players', 'direction')) {
$direction = ', `direction` = ' . $this->db->quote($this->data['direction']);
}
$blessings = '';
if($this->db->hasColumn('players', 'blessings')) {
$blessings = ', `blessings` = ' . $this->db->quote($this->data['blessings']);
}
$stamina = '';
if($this->db->hasColumn('players', 'stamina')) {
$stamina = ', `stamina` = ' . $this->db->quote($this->data['stamina']);
}
$lookaddons = '';
if($this->db->hasColumn('players', 'lookaddons')) {
$lookaddons = ', `lookaddons` = ' . $this->db->quote($this->data['lookaddons']);
}
// UPDATE query on database
$this->db->query('UPDATE ' . $this->db->tableName('players') . ' SET ' . $this->db->fieldName('name') . ' = ' . $this->db->quote($this->data['name']) . ', ' . $this->db->fieldName('account_id') . ' = ' . $this->data['account_id'] . ', ' . $this->db->fieldName('group_id') . ' = ' . $this->data['group_id'] . ', ' . $this->db->fieldName('sex') . ' = ' . $this->data['sex'] . ', ' . $this->db->fieldName('vocation') . ' = ' . $this->data['vocation'] . ', ' . $this->db->fieldName('experience') . ' = ' . $this->data['experience'] . ', ' . $this->db->fieldName('level') . ' = ' . $this->data['level'] . ', ' . $this->db->fieldName('maglevel') . ' = ' . $this->data['maglevel'] . ', ' . $this->db->fieldName('health') . ' = ' . $this->data['health'] . ', ' . $this->db->fieldName('healthmax') . ' = ' . $this->data['healthmax'] . ', ' . $this->db->fieldName('mana') . ' = ' . $this->data['mana'] . ', ' . $this->db->fieldName('manamax') . ' = ' . $this->data['manamax'] . ', ' . $this->db->fieldName('manaspent') . ' = ' . $this->data['manaspent'] . ', ' . $this->db->fieldName('soul') . ' = ' . $this->data['soul'] . ', ' . $this->db->fieldName('lookbody') . ' = ' . $this->data['lookbody'] . ', ' . $this->db->fieldName('lookfeet') . ' = ' . $this->data['lookfeet'] . ', ' . $this->db->fieldName('lookhead') . ' = ' . $this->data['lookhead'] . ', ' . $this->db->fieldName('looklegs') . ' = ' . $this->data['looklegs'] . ', ' . $this->db->fieldName('looktype') . ' = ' . $this->data['looktype'] . $lookaddons . ', ' . $this->db->fieldName('posx') . ' = ' . $this->data['posx'] . ', ' . $this->db->fieldName('posy') . ' = ' . $this->data['posy'] . ', ' . $this->db->fieldName('posz') . ' = ' . $this->data['posz'] . ', ' . $this->db->fieldName('cap') . ' = ' . $this->data['cap'] . ', ' . $this->db->fieldName('lastlogin') . ' = ' . $this->data['lastlogin'] . ', ' . $this->db->fieldName('lastlogout') . ' = ' . $this->data['lastlogout'] . ', ' . $this->db->fieldName('lastip') . ' = ' . $this->db->quote($this->data['lastip']) . ', ' . $this->db->fieldName('save') . ' = ' . (int) $this->data['save'] . ', ' . $this->db->fieldName('conditions') . ' = ' . $this->db->quote($this->data['conditions']) . ', `' . $skull_time . '` = ' . $this->data['skulltime'] . ', `' . $skull_type . '` = ' . (int) $this->data['skull'] . $guild_info . ', ' . $this->db->fieldName('town_id') . ' = ' . $this->data['town_id'] . $loss . $loss_items . ', ' . $this->db->fieldName('balance') . ' = ' . $this->data['balance'] . $blessings . $stamina . $direction . ' WHERE ' . $this->db->fieldName('id') . ' = ' . $this->data['id']);
if( isset($this->data['id']) ) {
PlayerModel::where('id', $this->data['id'])->update($values);
}
// creates new player
else
{
$loss = '';
$loss_data = '';
if($this->db->hasColumn('players', 'loss_experience')) {
$loss = ', `loss_experience`, `loss_mana`, `loss_skills`';
$loss_data = ', ' . $this->data['loss_experience'] . ', ' . $this->data['loss_mana'] . ', ' . $this->data['loss_skills'];
}
else {
$values['created'] = time();
$loss_items = '';
$loss_items_data = '';
if($this->db->hasColumn('players', 'loss_items')) {
$loss_items = ', `loss_items`, `loss_containers`';
$loss_items_data = ', ' . $this->data['loss_items'] . ', ' . $this->data['loss_containers'];
}
$player = PlayerModel::create($values);
$guild_info = '';
$guild_info_data = '';
if(!$this->db->hasTable('guild_members') && $this->db->hasColumn('players', 'guildnick')) {
$guild_info = ', `guildnick`, `rank_id`';
$guild_info_data = ', ' . $this->db->quote($this->data['guildnick']) . ', ' . $this->data['rank_id'];
}
$promotion = '';
$promotion_data = '';
if($this->db->hasColumn('players', 'promotion')) {
$promotion = ', `promotion`';
$promotion_data = ', ' . $this->data['promotion'];
}
$direction = '';
$direction_data = '';
if($this->db->hasColumn('players', 'direction')) {
$direction = ', `direction`';
$direction_data = ', ' . $this->data['direction'];
}
$blessings = '';
$blessings_data = '';
if($this->db->hasColumn('players', 'blessings')) {
$blessings = ', `blessings`';
$blessings_data = ', ' . $this->data['blessings'];
}
$stamina = '';
$stamina_data = '';
if($this->db->hasColumn('players', 'stamina')) {
$stamina = ', `stamina`';
$stamina_data = ', ' . $this->data['stamina'];
}
$lookaddons = '';
$lookaddons_data = '';
if($this->db->hasColumn('players', 'lookaddons')) {
$lookaddons = ', `lookaddons`';
$lookaddons_data = ', ' . $this->data['lookaddons'];
}
// INSERT query on database
$this->db->query('INSERT INTO `players` (`name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`' . $lookaddons . ', `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `save`, `conditions`, `' . $skull_time . '`, `' . $skull_type . '`' . $guild_info . ', `town_id`' . $loss . $loss_items . ', `balance`' . $blessings . $stamina . $direction . ', `created`' . $promotion . ', `comment`) VALUES (' . $this->db->quote($this->data['name']) . ', ' . $this->data['account_id'] . ', ' . $this->data['group_id'] . ', ' . $this->data['sex'] . ', ' . $this->data['vocation'] . ', ' . $this->data['experience'] . ', ' . $this->data['level'] . ', ' . $this->data['maglevel'] . ', ' . $this->data['health'] . ', ' . $this->data['healthmax'] . ', ' . $this->data['mana'] . ', ' . $this->data['manamax'] . ', ' . $this->data['manaspent'] . ', ' . $this->data['soul'] . ', ' . $this->data['lookbody'] . ', ' . $this->data['lookfeet'] . ', ' . $this->data['lookhead'] . ', ' . $this->data['looklegs'] . ', ' . $this->data['looktype'] . $lookaddons_data . ', ' . $this->data['posx'] . ', ' . $this->data['posy'] . ', ' . $this->data['posz'] . ', ' . $this->data['cap'] . ', ' . $this->data['lastlogin'] . ', ' . $this->data['lastlogout'] . ', ' . $this->data['lastip'] . ', ' . (int) $this->data['save'] . ', ' . $this->db->quote($this->data['conditions']) . ', ' . $this->data['skulltime'] . ', ' . (int) $this->data['skull'] . $guild_info_data . ', ' . $this->data['town_id'] . $loss_data . $loss_items_data . ', ' . $this->data['balance'] . $blessings_data . $stamina_data . $direction_data . ', ' . time() . $promotion_data . ', "")');
// ID of new group
$this->data['id'] = $this->db->lastInsertId();
// ID of new player
$this->data['id'] = $player->id;
}
// updates skills - doesn't matter if we have just created character - trigger inserts new skills
@@ -490,7 +316,7 @@ class OTS_Player extends OTS_Row_DAO
$set .= ',';
}
$skills = $this->db->query('UPDATE `players` SET ' . $set . ' WHERE `id` = ' . $this->data['id']);
$this->db->query('UPDATE `players` SET ' . $set . ' WHERE `id` = ' . $this->data['id']);
}
else if($this->db->hasTable('player_skills')) {
foreach($this->skills as $id => $skill)
@@ -748,21 +574,25 @@ class OTS_Player extends OTS_Row_DAO
public function isDeleted()
{
$field = 'deleted';
$column = 'deleted';
if($this->db->hasColumn('players', 'deletion'))
$field = 'deletion';
$column = 'deletion';
if( !isset($this->data[$field]) )
if( !isset($this->data[$column]) )
{
throw new E_OTS_NotLoaded();
}
return $this->data[$field] > 0;
return $this->data[$column] > 0;
}
public function setDeleted($deleted)
{
$this->data['deleted'] = (int) $deleted;
$column = 'deleted';
if($this->db->hasColumn('players', 'deletion'))
$column = 'deletion';
$this->data[$column] = (int) $deleted;
}
public function isOnline()
@@ -852,13 +682,7 @@ class OTS_Player extends OTS_Row_DAO
throw new E_OTS_NotLoaded();
}
if(isset($this->data['promotion'])) {
global $config;
if((int)$this->data['promotion'] > 0)
return ($this->data['vocation'] + ($this->data['promotion'] * $config['vocations_amount']));
}
return $this->data['vocation'];
return \OTS_Toolbox::getVocationFromPromotion($this->data['vocation'], $this->data['promotion'] ?? 0);
}
@@ -1574,12 +1398,7 @@ class OTS_Player extends OTS_Row_DAO
*/
public function getCap()
{
if( !isset($this->data['cap']) )
{
throw new E_OTS_NotLoaded();
}
return $this->data['cap'];
return $this->data['cap'] ?? 0;
}
/**
@@ -1792,12 +1611,12 @@ class OTS_Player extends OTS_Row_DAO
*/
public function getSkullTime()
{
if( !isset($this->data['skulltime']) )
{
throw new E_OTS_NotLoaded();
$column = 'skulltime';
if($this->db->hasColumn('players', 'skull_time')) {
$column = 'skull_time';
}
return $this->data['skulltime'];
return $this->data[$column] ?? 0;
}
/**
@@ -1811,7 +1630,12 @@ class OTS_Player extends OTS_Row_DAO
*/
public function setSkullTime($skulltime)
{
$this->data['skulltime'] = (int) $skulltime;
$column = 'skulltime';
if($this->db->hasColumn('players', 'skull_time')) {
$column = 'skull_time';
}
$this->data[$column] = (int) $skulltime;
}
/**
@@ -3250,6 +3074,10 @@ class OTS_Player extends OTS_Row_DAO
return 0;
}
public function setData(array $data): void{
$this->data = $data;
}
/**
* Magic PHP5 method.
*

View File

@@ -13,6 +13,8 @@
* @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU Lesser General Public License, Version 3
*/
use MyAAC\Server\XML\Vocations;
/**
* Toolbox for common operations.
*
@@ -110,14 +112,21 @@ class OTS_Toolbox
$list->setFilter($filter);
return $list;
}
public static function getVocationName($id, $promotion = 0): string
public static function getVocationFromPromotion($id, $promotion = 0): int
{
if($promotion > 0) {
$id = ($id + ($promotion * config('vocations_amount')));
for ($i = 0; $i < $promotion; $i++) {
if ($_id = Vocations::getPromoted($id)) {
$id = $_id;
}
}
}
return config('vocations')[$id] ?? 'Unknown';
return $id;
}
public static function getVocationName($id, $promotion = 0): string {
return config('vocations')[self::getVocationFromPromotion($id, $promotion)] ?? 'Unknown';
}
}

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

@@ -1,5 +1,6 @@
<?php
use MyAAC\Models\Player as PlayerModel;
use MyAAC\Settings;
function updateHighscoresIdsHidden(): void
@@ -10,12 +11,22 @@ function updateHighscoresIdsHidden(): void
return;
}
$query = $db->query("SELECT `id` FROM `players` WHERE (`name` = " . $db->quote("Rook Sample") . " OR `name` = " . $db->quote("Sorcerer Sample") . " OR `name` = " . $db->quote("Druid Sample") . " OR `name` = " . $db->quote("Paladin Sample") . " OR `name` = " . $db->quote("Knight Sample") . " OR `name` = " . $db->quote("Account Manager") . ") ORDER BY `id`;");
$players = PlayerModel::where('name', 'Rook Sample')
->orWhere('name', 'Sorcerer Sample')
->orWhere('name', 'Druid Sample')
->orWhere('name', 'Paladin Sample')
->orWhere('name', 'Knight Sample')
->orWhere('name', 'Monk Sample')
->orWhere('name', 'Account Manager')
->orderBy('id')
->select('id')
->get();
$highscores_ignored_ids = array();
if ($query->rowCount() > 0) {
foreach ($query->fetchAll() as $result)
$highscores_ignored_ids[] = $result['id'];
$highscores_ignored_ids = [];
if (count($players) > 0) {
foreach ($players as $result) {
$highscores_ignored_ids[] = $result->id;
}
} else {
$highscores_ignored_ids[] = 0;
}

42
system/migrations/47.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
/**
* @var OTS_DB_MySQL $db
*/
// 2025-02-27
// remove ipv6, change to ip (for both ipv4 + ipv6) as VARCHAR(45)
$up = function () use ($db) {
$accountActionsInfo = $db->getColumnInfo(TABLE_PREFIX . 'account_actions', 'account_id');
if ($accountActionsInfo && is_array($accountActionsInfo) && $accountActionsInfo['key'] == 'pri') {
$db->query("ALTER TABLE `myaac_account_actions` DROP KEY `account_id`;");
}
if (!$db->hasColumn(TABLE_PREFIX . 'account_actions', 'id')) {
$db->addColumn(TABLE_PREFIX . 'account_actions', 'id', 'INT NOT NULL AUTO_INCREMENT FIRST, ADD PRIMARY KEY (`id`)');
}
$db->modifyColumn(TABLE_PREFIX . 'account_actions', 'ip', "VARCHAR(45) NOT NULL DEFAULT ''");
$db->query("UPDATE `" . TABLE_PREFIX . "account_actions` SET `ip` = INET_NTOA(`ip`) WHERE `ip` != '0';");
$db->query("UPDATE `" . TABLE_PREFIX . "account_actions` SET `ip` = INET6_NTOA(`ipv6`) WHERE `ip` = '0';");
if ($db->hasColumn(TABLE_PREFIX . 'account_actions', 'ipv6')) {
$db->dropColumn(TABLE_PREFIX . 'account_actions', 'ipv6');
}
};
$down = function () use ($db) {
if ($db->hasColumn(TABLE_PREFIX . 'account_actions', 'id')) {
$db->query("ALTER TABLE `" . TABLE_PREFIX . "account_actions` DROP `id`;");
}
$db->query("ALTER TABLE `" . TABLE_PREFIX . "account_actions` ADD KEY (`account_id`);");
if (!$db->hasColumn(TABLE_PREFIX . 'account_actions', 'ipv6')) {
$db->addColumn(TABLE_PREFIX . 'account_actions', 'ipv6', "BINARY(16) NOT NULL DEFAULT 0x00000000000000000000000000000000 AFTER ip");
}
$db->query("UPDATE `" . TABLE_PREFIX . "account_actions` SET `ipv6` = INET6_ATON(ip) WHERE NOT IS_IPV4(`ip`);");
$db->query("UPDATE `" . TABLE_PREFIX . "account_actions` SET `ip` = INET_ATON(`ip`) WHERE IS_IPV4(`ip`);");
$db->query("UPDATE `" . TABLE_PREFIX . "account_actions` SET `ip` = 0 WHERE `ipv6` != 0x00000000000000000000000000000000;");
$db->modifyColumn(TABLE_PREFIX . 'account_actions', 'ip', "INT(11) UNSIGNED NOT NULL DEFAULT 0;");
};

16
system/migrations/48.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
/**
* @var OTS_DB_MySQL $db
*/
$up = function () use ($db) {
if (!$db->hasColumn(TABLE_PREFIX . 'menu', 'access')) {
$db->addColumn(TABLE_PREFIX . 'menu', 'access', 'TINYINT NOT NULL DEFAULT 0 AFTER `link`');
}
};
$down = function () use ($db) {
if ($db->hasColumn(TABLE_PREFIX . 'menu', 'access')) {
$db->dropColumn(TABLE_PREFIX . 'menu', 'access');
}
};

91
system/migrations/49.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
/**
* @var OTS_DB_MySQL $db
*/
use MyAAC\Models\Account as AccountModel;
$time = time();
$accountId = getSession('account') ?? 1;
if (!defined('MYAAC_INSTALL')) {
$accountModel = AccountModel::where('web_flags', 3)->first();
if ($accountModel) {
$accountId = $accountModel->id;
}
}
function insert_sample_if_not_exist($p): void
{
global $time, $accountId;
$player = new OTS_Player();
$player->find($p['name']);
if (!$player->isLoaded()) {
$player->setData([
'name' => $p['name'],
'group_id' => 1,
'account_id' => $accountId,
'level' => $p['level'],
'vocation' => $p['vocation_id'],
'health' => $p['health'],
'healthmax' => $p['healthmax'],
'experience' => $p['experience'],
'lookbody' => 118,
'lookfeet' => 114,
'lookhead' => 38,
'looklegs' => 57,
'looktype' => $p['looktype'],
'maglevel' => 0,
'mana' => $p['mana'],
'manamax' => $p['manamax'],
'manaspent' => 0,
'soul' => $p['soul'],
'town_id' => 1,
'posx' => 1000,
'posy' => 1000,
'posz' => 7,
'conditions' => '',
'cap' => $p['cap'],
'sex' => 1,
'lastlogin' => $time,
'lastip' => 2130706433,
'save' => 1,
'lastlogout' => $time,
'balance' => 0,
'created' => $time,
'hide' => 1,
'comment' => '',
]);
$player->save();
}
}
$up = function () use ($db) {
if (!$db->hasTable('players')) {
return;
}
insert_sample_if_not_exist(['name' => 'Rook Sample', 'level' => 1, 'vocation_id' => 0, 'health' => 150, 'healthmax' => 150, 'experience' => 0, 'looktype' => 130, 'mana' => 0, 'manamax' => 0, 'soul' => 100, 'cap' => 400]);
insert_sample_if_not_exist(['name' => 'Sorcerer Sample', 'level' => 8, 'vocation_id' => 1, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 130, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470]);
insert_sample_if_not_exist(['name' => 'Druid Sample', 'level' => 8, 'vocation_id' => 2, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 130, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470]);
insert_sample_if_not_exist(['name' => 'Paladin Sample', 'level' => 8, 'vocation_id' => 3, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 129, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470]);
insert_sample_if_not_exist(['name' => 'Knight Sample', 'level' => 8, 'vocation_id' => 4, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 131, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470]);
insert_sample_if_not_exist(['name' => 'Monk Sample', 'level' => 8, 'vocation_id' => 9, 'health' => 185, 'healthmax' => 185, 'experience' => 4200, 'looktype' => 128, 'mana' => 90, 'manamax' => 90, 'soul' => 100, 'cap' => 470]);
if (defined('MYAAC_INSTALL')) {
global $locale;
success($locale['step_database_imported_players']);
}
require_once __DIR__ . '/20.php';
updateHighscoresIdsHidden();
};
$down = function () {
// nothing
};

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS `myaac_gallery`
(
`id` int NOT NULL AUTO_INCREMENT,
`comment` varchar(255) NOT NULL DEFAULT '',
`image` varchar(255) NOT NULL,
`thumb` varchar(255) NOT NULL,
`author` varchar(50) NOT NULL DEFAULT '',
`ordering` int NOT NULL DEFAULT 0,
`hide` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

16
system/migrations/50.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
/**
* @var OTS_DB_MySQL $db
*/
$up = function () use ($db) {
if ($db->hasTable(TABLE_PREFIX . 'gallery')) {
$db->dropTable(TABLE_PREFIX . 'gallery');
}
};
$down = function () use ($db) {
if (!$db->hasTable(TABLE_PREFIX . 'gallery')) {
$db->query(file_get_contents(__DIR__ . '/50-gallery.sql'));
}
};

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;

36
system/migrations/51.php Normal file
View File

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

View File

@@ -0,0 +1,29 @@
<?php
/**
* Example of using getTopPlayers() function
* to display the best players for each skill
*/
defined('MYAAC') or die('Direct access not allowed!');
$skills = [
'magic', 'level',
'balance', 'frags',
POT::SKILL_FIST, POT::SKILL_CLUB,
POT::SKILL_SWORD, POT::SKILL_AXE,
POT::SKILL_DISTANCE, POT::SKILL_SHIELD,
POT::SKILL_FISH
];
foreach ($skills as $skill) {?>
<ul>
<?php
echo '<strong>' . ucwords(is_string($skill) ? $skill : getSkillName($skill)) . '</strong>';
foreach (getTopPlayers(5, $skill) as $player) {?>
<li><?= $player['rank'] . '. ' . $player['name'] . ' - ' . $player['value']; ?></li>
<?php
}
?>
</ul>
<?php
}

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

@@ -10,6 +10,7 @@
*/
use MyAAC\CreateCharacter;
use MyAAC\Models\AccountAction;
use MyAAC\Models\AccountEmailVerify;
defined('MYAAC') or die('Direct access not allowed!');
@@ -44,6 +45,16 @@ $errors = array();
$save = isset($_POST['save']) && $_POST['save'] == 1;
if($save)
{
$cooldown = setting('core.account_create_ip_block_cooldown');;
if ($cooldown > 0) {
$accountAction = AccountAction::where('ip', get_browser_real_ip())->where('action', 'Account created.')->where('date', '>=', time() - ($cooldown * 60))->first();
if ($accountAction) {
$minute = ($cooldown > 1 ? 'minutes' : 'minute');
$errors['account'] = "You have to wait $cooldown $minute before creating another account.";
}
}
if(!config('account_login_by_email')) {
if(USE_ACCOUNT_NAME) {
$account_name = $_POST['account'];
@@ -140,7 +151,7 @@ if($save)
'country' => $country,
'password' => $password,
'password_confirm' => $password_confirm,
'accept_rules' => isset($_POST['accept_rules']) ? $_POST['accept_rules'] === 'true' : false,
'accept_rules' => isset($_POST['accept_rules']) && $_POST['accept_rules'] === 'true',
);
if (!config('account_login_by_email')) {
@@ -192,6 +203,21 @@ if($save)
$new_account->setPassword(encrypt($password));
$new_account->setEMail($email);
$settingAccountPremiumDays = setting('core.account_premium_days');
if($settingAccountPremiumDays && $settingAccountPremiumDays > 0) {
$new_account->setPremDays($settingAccountPremiumDays);
if (!isCanary()) {
$lastDay = 0;
if($settingAccountPremiumDays != 0 && $settingAccountPremiumDays != OTS_Account::GRATIS_PREMIUM_DAYS) {
$lastDay = time();
}
$new_account->setLastLogin($lastDay);
}
}
$new_account->save();
$hooks->trigger(HOOK_ACCOUNT_CREATE_AFTER_SAVED, ['account' => $new_account]);
@@ -206,22 +232,6 @@ if($save)
$new_account->setCustomField('country', $country);
}
$settingAccountPremiumDays = setting('core.account_premium_days');
if($settingAccountPremiumDays && $settingAccountPremiumDays > 0) {
if($db->hasColumn('accounts', 'premend')) { // othire
$new_account->setCustomField('premend', time() + $settingAccountPremiumDays * 86400);
}
else { // rest
if ($db->hasColumn('accounts', 'premium_ends_at')) { // TFS 1.4+
$new_account->setCustomField('premium_ends_at', time() + $settingAccountPremiumDays * (60 * 60 * 24));
}
else {
$new_account->setCustomField('premdays', $settingAccountPremiumDays);
$new_account->setCustomField('lastday', time());
}
}
}
$accountDefaultPremiumPoints = setting('core.account_premium_points');
if($accountDefaultPremiumPoints > 0) {
$new_account->setCustomField('premium_points', $accountDefaultPremiumPoints);

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

@@ -9,540 +9,11 @@
* @link https://my-aac.org
*/
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Lost Account Interface';
$title = 'Lost Account';
if(!setting('core.mail_enabled'))
{
echo '<b>Account maker is not configured to send e-mails, you can\'t use Lost Account Interface. Contact with admin to get help.</b>';
if(!setting('core.mail_enabled')) {
echo "<b>Account maker is not configured to send e-mails, you can't use Lost Account Interface. Contact with admin to get help.</b>";
return;
}
$action_type = isset($_REQUEST['action_type']) ? $_REQUEST['action_type'] : '';
if($action == '')
{
$twig->display('account.lost.form.html.twig');
}
else if($action == 'step1' && $action_type == '') {
$twig->display('account.lost.noaction.html.twig');
}
elseif($action == 'step1' && $action_type == 'email')
{
$nick = stripslashes($_REQUEST['nick']);
if(Validator::characterName($nick))
{
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded())
$account = $player->getAccount();
if($account->isLoaded())
{
if($account->getCustomField('email_next') < time())
echo 'Please enter e-mail to account with this character.<BR>
<form action="' . getLink('account/lost') . '?action=sendcode" method=post>
<input type=hidden name="character">
<table cellspacing=1 cellpadding=4 border=0 width=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Please enter e-mail to account</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
Character: <INPUT TYPE=text NAME="nick" VALUE="'.$nick.'" SIZE="40" readonly="readonly"><BR>
E-mail to account:<INPUT TYPE=text NAME="email" VALUE="" SIZE="40"><BR>
</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
' . $twig->render('buttons.submit.html.twig') . '</div>
</TD></TR></FORM></TABLE></TABLE>';
else
{
$insec = (int)$account->getCustomField('email_next') - time();
$minutesleft = floor($insec / 60);
$secondsleft = $insec - ($minutesleft * 60);
$timeleft = $minutesleft.' minutes '.$secondsleft.' seconds';
echo 'Account of selected character (<b>'.$nick.'</b>) received e-mail in last '.ceil(setting('core.mail_lost_account_interval') / 60).' minutes. You must wait '.$timeleft.' before you can use Lost Account Interface again.';
}
}
else
echo 'Player or account of player <b>' . $nick . '</b> doesn\'t exist.';
}
else
echo 'Invalid player name format. If you have other characters on account try with other name.';
echo '<BR /><TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<a href="' . getLink('account/lost') . '" border="0"><IMG SRC="'.$template_path.'/images/global/buttons/sbutton_back.gif" NAME="Back" ALT="Back" BORDER=0 WIDTH=120 HEIGHT=18></a></div>
</TD></TR></FORM></TABLE></TABLE>';
}
elseif($action == 'sendcode')
{
$email = $_REQUEST['email'];
$nick = stripslashes($_REQUEST['nick']);
if(Validator::characterName($nick))
{
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded())
$account = $player->getAccount();
if($account->isLoaded())
{
if($account->getCustomField('email_next') < time())
{
if($account->getEMail() == $email)
{
$newcode = generateRandomString(30, true, false, true);
$mailBody = '
You asked to reset your ' . $config['lua']['serverName'] . ' password.<br/>
<p>Account name: '.$account->getName().'</p>
<br />
To do so, please click this link:
<p><a href="' . getLink('account/lost') . '?action=checkcode&code='.$newcode.'&character='.urlencode($nick).'">' . getLink('account/lost') . '?action=checkcode&code='.$newcode.'&character='.urlencode($nick).'</a></p>
<p>or open page: <i>' . getLink('account/lost') . '?action=checkcode</i> and in field "code" write <b>'.$newcode.'</b></p>
<br/>
<p>If you did not request a password change, you may ignore this message and your password will remain unchanged.';
$account_mail = $account->getCustomField('email');
if(_mail($account_mail, $config['lua']['serverName'].' - Recover your account', $mailBody))
{
$account->setCustomField('email_code', $newcode);
$account->setCustomField('email_next', (time() + setting('core.mail_lost_account_interval')));
echo '<br />Details about steps required to recover your account has been sent to <b>' . $account_mail . '</b>. You should receive this email within 15 minutes. Please check your inbox/spam directory.';
}
else
{
$account->setCustomField('email_next', (time() + 60));
echo '<br /><p class="error">An error occurred while sending email! Try again later or contact with admin. For Admin: More info can be found in system/logs/mailer-error.log</p>';
}
}
else
echo 'Invalid e-mail to account of character <b>'.$nick.'</b>. Try again.';
}
else
{
$insec = (int)$account->getCustomField('email_next') - time();
$minutesleft = floor($insec / 60);
$secondsleft = $insec - ($minutesleft * 60);
$timeleft = $minutesleft.' minutes '.$secondsleft.' seconds';
echo 'Account of selected character (<b>'.$nick.'</b>) received e-mail in last '.ceil(setting('core.mail_lost_account_interval') / 60).' minutes. You must wait '.$timeleft.' before you can use Lost Account Interface again.';
}
}
else
echo 'Player or account of player <b>'.$nick.'</b> doesn\'t exist.';
}
else
echo 'Invalid player name format. If you have other characters on account try with other name.';
echo '<BR /><TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<a href="' . getLink('account/lost') . '?action=step1&action_type=email&nick='.urlencode($nick).'" border="0"><IMG SRC="'.$template_path.'/images/global/buttons/sbutton_back.gif" NAME="Back" ALT="Back" BORDER=0 WIDTH=120 HEIGHT=18></a></div>
</TD></TR></FORM></TABLE></TABLE>';
}
elseif($action == 'step1' && $action_type == 'reckey')
{
$nick = stripslashes($_REQUEST['nick']);
if(Validator::characterName($nick))
{
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded())
$account = $player->getAccount();
if($account->isLoaded())
{
$account_key = $account->getCustomField('key');
if(!empty($account_key))
{
echo 'If you enter right recovery key you will see form to set new e-mail and password to account. To this e-mail will be send your new password and account name.<BR>
<FORM ACTION="' . getLink('account/lost') . '?action=step2" METHOD=post>
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Please enter your recovery key</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
Character name:&nbsp;<INPUT TYPE=text NAME="nick" VALUE="'.$nick.'" SIZE="40" readonly="readonly"><BR />
Recovery key:&nbsp;&nbsp;&nbsp;&nbsp;<INPUT TYPE=text NAME="key" VALUE="" SIZE="40"><BR>
</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
' . $twig->render('buttons.submit.html.twig') . '</div>
</TD></TR></FORM></TABLE></TABLE>';
}
else
echo 'Account of this character has no recovery key!';
}
else
echo 'Player or account of player <b>'.$nick.'</b> doesn\'t exist.';
}
else
echo 'Invalid player name format. If you have other characters on account try with other name.';
echo '<BR /><TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<a href="' . getLink('account/lost') . '" border="0"><IMG SRC="'.$template_path.'/images/global/buttons/sbutton_back.gif" NAME="Back" ALT="Back" BORDER=0 WIDTH=120 HEIGHT=18></a></div>
</TD></TR></FORM></TABLE></TABLE>';
}
elseif($action == 'step2')
{
$rec_key = trim($_REQUEST['key']);
$nick = stripslashes($_REQUEST['nick']);
if(Validator::characterName($nick))
{
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded())
$account = $player->getAccount();
if($account->isLoaded())
{
$account_key = $account->getCustomField('key');
if(!empty($account_key))
{
if($account_key == $rec_key)
{
echo '<script type="text/javascript">
function validate_required(field,alerttxt)
{
with (field)
{
if (value==null||value==""||value==" ")
{alert(alerttxt);return false;}
else {return true}
}
}
function validate_email(field,alerttxt)
{
with (field)
{
apos=value.indexOf("@");
dotpos=value.lastIndexOf(".");
if (apos<1||dotpos-apos<2)
{alert(alerttxt);return false;}
else {return true;}
}
}
function validate_form(thisform)
{
with (thisform)
{
if (validate_required(email,"Please enter your e-mail!")==false)
{email.focus();return false;}
if (validate_email(email,"Invalid e-mail format!")==false)
{email.focus();return false;}
if (validate_required(passor,"Please enter password!")==false)
{passor.focus();return false;}
if (validate_required(passor2,"Please repeat password!")==false)
{passor2.focus();return false;}
if (passor2.value!=passor.value)
{alert(\'Repeated password is not equal to password!\');return false;}
}
}
</script>';
echo 'Set new password and e-mail to your account.<BR>
<FORM ACTION="' . getLink('account/lost') . '?action=step3" onsubmit="return validate_form(this)" METHOD=post>
<INPUT TYPE=hidden NAME="character" VALUE="">
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Please enter new password and e-mail</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
Account of character:&nbsp;&nbsp;<INPUT TYPE=text NAME="nick" VALUE="'.$nick.'" SIZE="40" readonly="readonly"><BR />
New password:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<INPUT id="passor" TYPE=password NAME="passor" VALUE="" SIZE="40"><BR>
Repeat new password:&nbsp;&nbsp;<INPUT id="passor2" TYPE=password NAME="passor" VALUE="" SIZE="40"><BR>
New e-mail address:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<INPUT id="email" TYPE=text NAME="email" VALUE="" SIZE="40"><BR>
<INPUT TYPE=hidden NAME="key" VALUE="'.$rec_key.'">
</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
' . $twig->render('buttons.submit.html.twig') . '</div>
</TD></TR></FORM></TABLE></TABLE>';
}
else
echo 'Wrong recovery key!';
}
else
echo 'Account of this character has no recovery key!';
}
else
echo 'Player or account of player <b>'.$nick.'</b> doesn\'t exist.';
}
else
echo 'Invalid player name format. If you have other characters on account try with other name.';
echo '<BR /><TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<a href="' . getLink('account/lost') . '?action=step1&action_type=reckey&nick='.urlencode($nick).'" border="0"><IMG SRC="'.$template_path.'/images/global/buttons/sbutton_back.gif" NAME="Back" ALT="Back" BORDER=0 WIDTH=120 HEIGHT=18></a></div>
</TD></TR></FORM></TABLE></TABLE>';
}
elseif($action == 'step3')
{
$rec_key = trim($_REQUEST['key']);
$nick = stripslashes($_REQUEST['nick']);
$new_pass = trim($_REQUEST['passor']);
$new_email = trim($_REQUEST['email']);
if(Validator::characterName($nick))
{
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded())
$account = $player->getAccount();
if($account->isLoaded())
{
$account_key = $account->getCustomField('key');
if(!empty($account_key))
{
if($account_key == $rec_key)
{
if(Validator::password($new_pass))
{
if(Validator::email($new_email))
{
$account->setEMail($new_email);
$tmp_new_pass = $new_pass;
if(USE_ACCOUNT_SALT)
{
$salt = generateRandomString(10, false, true, true);
$tmp_new_pass = $salt . $new_pass;
}
$account->setPassword(encrypt($tmp_new_pass));
$account->save();
if(USE_ACCOUNT_SALT)
$account->setCustomField('salt', $salt);
echo 'Your account name, new password and new e-mail.<BR>
<FORM ACTION="' . getLink('account/manage') . '" onsubmit="return validate_form(this)" METHOD=post>
<INPUT TYPE=hidden NAME="character" VALUE="">
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Your account name, new password and new e-mail</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
Account name:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>'.$account->getName().'</b><BR>
New password:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>'.$new_pass.'</b><BR>
New e-mail address:&nbsp;<b>'.$new_email.'</b><BR>';
if($account->getCustomField('email_next') < time())
{
$mailBody = '
<h3>Your account name and new password!</h3>
<p>Changed password and e-mail to your account in Lost Account Interface on server <a href="'.BASE_URL.'"><b>'.$config['lua']['serverName'].'</b></a></p>
<p>Account name: <b>'.$account->getName().'</b></p>
<p>New password: <b>'.$new_pass.'</b></p>
<p>E-mail: <b>'.$new_email.'</b> (this e-mail)</p>
<br />
<p><u>It\'s automatic e-mail from OTS Lost Account System. Do not reply!</u></p>';
if(_mail($account->getCustomField('email'), $config['lua']['serverName']." - New password to your account", $mailBody))
{
echo '<br /><small>Sent e-mail with your account name and password to new e-mail. You should receive this e-mail in 15 minutes. You can login now with new password!</small>';
}
else
{
echo '<br /><p class="error">An error occurred while sending email! You will not receive e-mail with this informations. For Admin: More info can be found in system/logs/mailer-error.log</p>';
}
}
else
{
echo '<br /><small>You will not receive e-mail with this informations.</small>';
}
echo '<INPUT TYPE=hidden NAME="account_login" VALUE="'.$account->getId().'">
<INPUT TYPE=hidden NAME="password_login" VALUE="'.$new_pass.'">
</TD></TR></TABLE><BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<INPUT TYPE=image NAME="Login" ALT="Login" SRC="'.$template_path.'/images/global/buttons/sbutton_login.gif" BORDER=0 WIDTH=120 HEIGHT=18></div>
</TD></TR></FORM></TABLE></TABLE>';
}
else
echo Validator::getLastError();
}
else
echo Validator::getLastError();
}
else
echo 'Wrong recovery key!';
}
else
echo 'Account of this character has no recovery key!';
}
else
echo 'Player or account of player <b>'.$nick.'</b> doesn\'t exist.';
}
else
echo 'Invalid player name format. If you have other characters on account try with other name.';
echo '<BR /><TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<a href="' . getLink('account/lost') . '?action=step1&action_type=reckey&nick='.urlencode($nick).'" border="0"><IMG SRC="'.$template_path.'/images/global/buttons/sbutton_back.gif" NAME="Back" ALT="Back" BORDER=0 WIDTH=120 HEIGHT=18></a></div>
</TD></TR></FORM></TABLE></TABLE>';
}
elseif($action == 'checkcode')
{
$code = trim($_REQUEST['code']);
$character = stripslashes(trim($_REQUEST['character']));
if(empty($code) || empty($character))
echo 'Please enter code from e-mail and name of one character from account. Then press Submit.<BR>
<FORM ACTION="' . getLink('account/lost') . '?action=checkcode" METHOD=post>
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Code & character name</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
Your code:&nbsp;<INPUT TYPE=text NAME="code" VALUE="" SIZE="40")><BR />
Character:&nbsp;<INPUT TYPE=text NAME="character" VALUE="" SIZE="40")><BR />
</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
' . $twig->render('buttons.submit.html.twig') . '</div>
</TD></TR></FORM></TABLE></TABLE>';
else
{
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($character);
if($player->isLoaded())
$account = $player->getAccount();
if($account->isLoaded())
{
if($account->getCustomField('email_code') == $code)
{
echo '<script type="text/javascript">
function validate_required(field,alerttxt)
{
with (field)
{
if (value==null||value==""||value==" ")
{alert(alerttxt);return false;}
else {return true}
}
}
function validate_form(thisform)
{
with (thisform)
{
if (validate_required(passor,"Please enter password!")==false)
{passor.focus();return false;}
if (validate_required(passor2,"Please repeat password!")==false)
{passor2.focus();return false;}
if (passor2.value!=passor.value)
{alert(\'Repeated password is not equal to password!\');return false;}
}
}
</script>
Please enter new password to your account and repeat to make sure you remember password.<BR>
<FORM ACTION="' . getLink('account/lost') . '?action=setnewpassword" onsubmit="return validate_form(this)" METHOD=post>
<INPUT TYPE=hidden NAME="character" VALUE="'.$character.'">
<INPUT TYPE=hidden NAME="code" VALUE="'.$code.'">
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Code & account name</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
New password:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<INPUT TYPE=password ID="passor" NAME="passor" VALUE="" SIZE="40")><BR />
Repeat new password:&nbsp;<INPUT TYPE=password ID="passor2" NAME="passor2" VALUE="" SIZE="40")><BR />
</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
' . $twig->render('buttons.submit.html.twig') . '</div>
</TD></TR></FORM></TABLE></TABLE>';
}
else
$error= 'Wrong code to change password.';
}
else
$error = 'Account of this character or this character doesn\'t exist.';
}
if(!empty($error))
echo '<span style="color: red"><b>'.$error.'</b></span><br />Please enter code from e-mail and name of one character from account. Then press Submit.<BR>
<FORM ACTION="' . getLink('account/lost') . '?action=checkcode" METHOD=post>
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Code & character name</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
Your code:&nbsp;<INPUT TYPE=text NAME="code" VALUE="" SIZE="40")><BR />
Character:&nbsp;<INPUT TYPE=text NAME="character" VALUE="" SIZE="40")><BR />
</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
' . $twig->render('buttons.submit.html.twig') . '</div>
</TD></TR></FORM></TABLE></TABLE>';
}
elseif($action == 'setnewpassword')
{
$newpassword = $_REQUEST['passor'];
$code = $_REQUEST['code'];
$character = stripslashes($_REQUEST['character']);
echo '';
if(empty($code) || empty($character) || empty($newpassword))
echo '<span style="color: red"><b>Error. Try again.</b></span><br />Please enter code from e-mail and name of one character from account. Then press Submit.<BR>
<BR><FORM ACTION="' . getLink('account/lost') . '?action=checkcode" METHOD=post>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<INPUT TYPE=image NAME="Back" ALT="Back" SRC="'.$template_path.'/images/global/buttons/sbutton_back.gif" BORDER=0 WIDTH=120 HEIGHT=18></div>
</TD></TR></FORM></TABLE></TABLE>';
else
{
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($character);
if($player->isLoaded())
$account = $player->getAccount();
if($account->isLoaded())
{
if($account->getCustomField('email_code') == $code)
{
if(Validator::password($newpassword))
{
$tmp_new_pass = $newpassword;
if(USE_ACCOUNT_SALT)
{
$salt = generateRandomString(10, false, true, true);
$tmp_new_pass = $salt . $newpassword;
$account->setCustomField('salt', $salt);
}
$account->setPassword(encrypt($tmp_new_pass ));
$account->save();
$account->setCustomField('email_code', '');
echo 'New password to your account is below. Now you can login.<BR>
<INPUT TYPE=hidden NAME="character" VALUE="'.$character.'">
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Changed password</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
New password:&nbsp;<b>'.$newpassword.'</b><BR />
Account name:&nbsp;&nbsp;&nbsp;<i>(Already on your e-mail)</i><BR />';
$mailBody = '
<h3>Your account name and password!</h3>
<p>Changed password to your account in Lost Account Interface on server <a href="'.BASE_URL.'"><b>'.$config['lua']['serverName'].'</b></a></p>
<p>Account name: <b>'.$account->getName().'</b></p>
<p>New password: <b>'.$newpassword.'</b></p>
<br />
<p><u>It\'s automatic e-mail from OTS Lost Account System. Do not reply!</u></p>';
if(_mail($account->getCustomField('email'), $config['lua']['serverName']." - Your new password", $mailBody))
{
echo '<br /><small>New password work! Sent e-mail with your password and account name. You should receive this e-mail in 15 minutes. You can login now with new password!';
}
else
{
echo '<br /><p class="error">New password work! An error occurred while sending email! You will not receive e-mail with new password. For Admin: More info can be found in system/logs/mailer-error.log';
}
echo '</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
<FORM ACTION="' . getLink('account/manage') . '" METHOD=post>
<INPUT TYPE=image NAME="Login" ALT="Login" SRC="'.$template_path.'/images/global/buttons/sbutton_login.gif" BORDER=0 WIDTH=120 HEIGHT=18></div>
</TD></TR></FORM></TABLE></TABLE>';
}
else
$error= Validator::getLastError();
}
else
$error= 'Wrong code to change password.';
}
else
$error = 'Account of this character or this character doesn\'t exist.';
}
if(!empty($error))
echo '<span style="color: red"><b>'.$error.'</b></span><br />Please enter code from e-mail and name of one character from account. Then press Submit.<BR>
<FORM ACTION="' . getLink('account/lost') . '?action=checkcode" METHOD=post>
<TABLE CELLSPACING=1 CELLPADDING=4 BORDER=0 WIDTH=100%>
<TR><TD BGCOLOR="'.$config['vdarkborder'].'" class="white"><B>Code & character name</B></TD></TR>
<TR><TD BGCOLOR="'.$config['darkborder'].'">
Your code:&nbsp;<INPUT TYPE=text NAME="code" VALUE="" SIZE="40")><BR />
Character:&nbsp;<INPUT TYPE=text NAME="character" VALUE="" SIZE="40")><BR />
</TD></TR>
</TABLE>
<BR>
<TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0 WIDTH=100%><TR><TD><div style="text-align:center">
' . $twig->render('buttons.submit.html.twig') . '</div>
</TD></TR></FORM></TABLE></TABLE>';
}
$twig->display('account/lost/form.html.twig');

View File

@@ -0,0 +1,18 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
function lostAccountWriteCooldown(string $nick, int $time): void
{
global $twig;
$inSec = $time - time();
$minutesLeft = floor($inSec / 60);
$secondsLeft = $inSec - ($minutesLeft * 60);
$timeLeft = "$minutesLeft minutes $secondsLeft seconds";
$timeRounded = ceil(setting('core.mail_lost_account_interval') / 60);
$twig->display('error_box.html.twig', [
'errors' => ["Account of selected character (<b>" . escapeHtml($nick) . "</b>) received e-mail in last $timeRounded minutes. You must wait $timeLeft before you can use Lost Account Interface again."]
]);
}

View File

@@ -0,0 +1,51 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
csrfProtect();
$title = 'Lost Account';
$code = $_REQUEST['code'] ?? '';
$character = $_REQUEST['character'] ?? '';
if(empty($code) || empty($character)) {
$twig->display('account/lost/check-code.html.twig', [
'code' => $code,
'characters' => $character,
]);
}
else {
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($character);
if($player->isLoaded()) {
$account = $player->getAccount();
}
if($account->isLoaded()) {
if($account->getCustomField('email_code') == $code) {
$twig->display('account/lost/check-code.finish.html.twig', [
'character' => $character,
'code' => $code,
]);
}
else {
$error = 'Wrong code to change password.';
}
}
else {
$error = "Account of this character or this character doesn't exist.";
}
}
if(!empty($error)) {
$twig->display('error_box.html.twig', [
'errors' => [$error],
]);
echo '<br/>';
$twig->display('account/lost/check-code.html.twig', [
]);
}

View File

@@ -0,0 +1,75 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
csrfProtect();
require __DIR__ . '/../base.php';
$title = 'Lost Account';
$email = $_POST['email'] ?? '';
$nick = $_POST['nick'] ?? '';
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded()) {
$account = $player->getAccount();
}
if($account->isLoaded()) {
if($account->getCustomField('email_next') < time()) {
if($account->getEMail() == $email) {
$newCode = generateRandomString(30, true, false, true);
$mailBody = $twig->render('mail.account.lost.code.html.twig', [
'newCode' => $newCode,
'account' => $account,
'nick' => $nick,
]);
$accountEMail = $account->getCustomField('email');
if(_mail($accountEMail, configLua('serverName') . ' - Recover your account', $mailBody)) {
$account->setCustomField('email_code', $newCode);
$account->setCustomField('email_next', (time() + setting('core.mail_lost_account_interval')));
$twig->display('success.html.twig', [
'title' => 'Email has been sent',
'description' => 'Details about steps required to recover your account has been sent to <b>' . $accountEMail . '</b>. You should receive this email within 15 minutes. Please check your inbox/spam directory.',
'custom_buttons' => '',
]);
$twig->display('account.back_button.html.twig', [
'new_line' => true,
'center' => true,
'action' => getLink('news'),
]);
return;
}
$account->setCustomField('email_next', (time() + 60));
error('An error occurred while sending email! Try again later or contact with admin. For Admin: More info can be found in system/logs/mailer-error.log</p>');
}
else {
$errors[] = 'Invalid e-mail to account of character <b>' . escapeHtml($nick) . '</b>. Try again.';
}
}
else {
lostAccountWriteCooldown($nick, (int)$account->getCustomField('email_next'));
}
}
else {
$errors[] = "Player or account of player <b>" . escapeHtml($nick) . "</b> doesn't exist.";
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
}
$twig->display('account.back_button.html.twig', [
'new_line' => true,
'center' => true,
'action' => getLink('account/lost/step-1') . '?action=email&nick=' . urlencode($nick),
]);

View File

@@ -0,0 +1,128 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
csrfProtect();
$title = 'Lost Account';
$newPassword = $_POST['password'] ?? '';
$passwordRepeat = $_POST['password_repeat'] ?? '';
$code = $_POST['code'] ?? '';
$character = $_POST['character'] ?? '';
if(empty($code) || empty($character)) {
$errors[] = 'Please enter code from e-mail and name of one character from account.';
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
$twig->display('account/lost/check-code.html.twig', [
'code' => $code,
'character' => $character,
]);
$twig->display('account.back_button.html.twig', [
'new_line' => true,
'center' => true,
'action' => getLink('account/lost/check-code')
]);
return;
}
if (empty($newPassword) || empty($passwordRepeat)) {
$errors[] = 'Please enter both passwords.';
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
$twig->display('account/lost/check-code.finish.html.twig', [
'character' => $character,
'code' => $code,
]);
return;
}
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($character);
if($player->isLoaded()) {
$account = $player->getAccount();
}
$passwordFailed = false;
if($account->isLoaded()) {
if($account->getCustomField('email_code') == $code) {
if ($newPassword == $passwordRepeat) {
if (Validator::password($newPassword)) {
$hooks->trigger(HOOK_ACCOUNT_LOST_EMAIL_SET_NEW_PASSWORD_POST);
if (empty($errors)) {
$tmp_new_pass = $newPassword;
if (USE_ACCOUNT_SALT) {
$salt = generateRandomString(10, false, true, true);
$tmp_new_pass = $salt . $newPassword;
$account->setCustomField('salt', $salt);
}
$account->setPassword(encrypt($tmp_new_pass));
$account->save();
$account->setCustomField('email_code', '');
$mailBody = $twig->render('mail.account.lost.new-password.html.twig', [
'account' => $account,
'newPassword' => $newPassword,
]);
$statusMsg = '';
if (_mail($account->getCustomField('email'), configLua('serverName') . ' - Your new password', $mailBody)) {
$statusMsg = '<br /><small>New password work! Sent e-mail with your password and account name. You should receive this e-mail in 15 minutes. You can login now with new password!';
} else {
$statusMsg = '<br /><p class="error">New password work! An error occurred while sending email! You will not receive e-mail with new password. For Admin: More info can be found in system/logs/mailer-error.log';
}
$twig->display('account/lost/finish.new-password.html.twig', [
'statusMsg' => $statusMsg,
'newPassword' => $newPassword,
]);
}
} else {
$passwordFailed = true;
$errors[] = Validator::getLastError();
}
}
else {
$passwordFailed = true;
$errors[] = 'Passwords are not the same!';
}
}
else {
$errors[] = 'Wrong code to change password.';
}
}
else {
$errors[] = "Account of this character or this character doesn't exist.";
}
if(!empty($errors)) {
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
echo '<br/>';
$template = 'account/lost/check-code.html.twig';
if($passwordFailed) {
$template = 'account/lost/check-code.finish.html.twig';
}
$twig->display($template, [
'code' => $code,
'character' => $character,
]);
}

View File

@@ -0,0 +1,36 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
require __DIR__ . '/../base.php';
csrfProtect();
$title = 'Lost Account';
$nick = $_REQUEST['nick'] ?? '';
if($account->isLoaded()) {
if($account->getCustomField('email_next') < time()) {
$twig->display('account/lost/email.html.twig', [
'nick' => $nick,
]);
}
else {
lostAccountWriteCooldown($nick, (int)$account->getCustomField('email_next'));
}
}
else {
$errors[] = "Player or account of player <b>" . escapeHtml($nick) . "</b> doesn't exist.";
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
}
$twig->display('account.back_button.html.twig', [
'new_line' => true,
'center' => true,
'action' => getLink('account/lost'),
]);

View File

@@ -0,0 +1,38 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
csrfProtect();
$title = 'Lost Account';
$nick = $_REQUEST['nick'] ?? '';
$key = $_REQUEST['key'] ?? '';
if($account->isLoaded()) {
$account_key = $account->getCustomField('key');
if(!empty($account_key)) {
$twig->display('account/lost/recovery-key.step-1.html.twig', [
'nick' => $nick,
'key' => $key,
]);
}
else {
$errors[] = 'Account of this character has no recovery key!';
}
}
else {
$errors[] = "Player or account of player <b>" . escapeHtml($nick) . "</b> doesn't exist.";
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
}
$twig->display('account.back_button.html.twig', [
'new_line' => true,
'center' => true,
'action' => getLink('account/lost'),
]);

View File

@@ -0,0 +1,49 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
csrfProtect();
$title = 'Lost Account';
$key = $_REQUEST['key'] ?? '';
$nick = $_REQUEST['nick'] ?? '';
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded()) {
$account = $player->getAccount();
}
if($account->isLoaded()) {
$accountKey = $account->getCustomField('key');
if(!empty($accountKey)) {
if($accountKey == $key) {
$twig->display('account/lost/recovery-key.step-2.html.twig', [
'nick' => $nick,
'key' => $key,
]);
}
else {
$errors[] = 'Wrong recovery key!';
}
}
else {
$errors[] = 'Account of this character has no recovery key!';
}
}
else {
$errors[] = "Player or account of player <b>" . escapeHtml($nick) . "</b> doesn't exist.";
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
}
$twig->display('account.back_button.html.twig', [
'new_line' => true,
'center' => true,
'action' => getLink('account/lost/step-1') . '?action=recovery-key&nick=' . urlencode($nick) . '&key=' . urlencode($key),
]);

View File

@@ -0,0 +1,117 @@
<?php
use MyAAC\Models\Account as AccountModel;
defined('MYAAC') or die('Direct access not allowed!');
csrfProtect();
$title = 'Lost Account';
$key = $_POST['key'];
$nick = $_POST['nick'] ?? '';
$newPassword = $_POST['password'] ?? '';
$passwordRepeat = $_POST['password_repeat'] ?? '';
$newEmail = $_POST['email'] ?? '';
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded()) {
$account = $player->getAccount();
}
if($account->isLoaded()) {
$accountKey = $account->getCustomField('key');
if(!empty($accountKey)) {
if($accountKey == $key) {
if(Validator::password($newPassword)) {
if ($newPassword == $passwordRepeat) {
if (Validator::email($newEmail)) {
$emailExists = AccountModel::where('email', $newEmail)->count() > 0;
if (!$emailExists) {
$hooks->trigger(HOOK_ACCOUNT_LOST_RECOVERY_KEY_STEP_3_POST);
if (empty($errors)) {
$account->setEMail($newEmail);
$tmp_new_pass = $newPassword;
if (USE_ACCOUNT_SALT) {
$salt = generateRandomString(10, false, true, true);
$tmp_new_pass = $salt . $newPassword;
}
$account->setPassword(encrypt($tmp_new_pass));
$account->save();
if (USE_ACCOUNT_SALT) {
$account->setCustomField('salt', $salt);
}
$statusMsg = '';
if ($account->getCustomField('email_next') < time()) {
$mailBody = $twig->render('mail.account.lost.new-email.html.twig', [
'account' => $account,
'newPassword' => $newPassword,
'newEmail' => $newEmail,
]);
if (_mail($account->getCustomField('email'), configLua('serverName') . ' - New password to your account', $mailBody)) {
$statusMsg = '<br /><small>Sent e-mail with your account name and password to new e-mail. You should receive this e-mail in 15 minutes. You can login now with new password!</small>';
} else {
$statusMsg = '<br /><p class="error">An error occurred while sending email! You will not receive e-mail with this informations. For Admin: More info can be found in system/logs/mailer-error.log</p>';
}
} else {
$statusMsg = '<br /><small>You will not receive e-mail with this informations.</small>';
}
$twig->display('account/lost/finish.new-email.html.twig', [
'statusMsg' => $statusMsg,
'account' => $account,
'newPassword' => $newPassword,
'newEmail' => $newEmail,
]);
return;
}
}
else {
$errors[] = 'This email is already registered!';
}
} else {
$errors[] = Validator::getLastError();
}
}
else {
$errors[] = 'Passwords are not the same!';
}
}
else {
$errors[] = Validator::getLastError();
}
}
else {
$errors[] = 'Wrong recovery key!';
}
}
else {
$errors[] = 'Account of this character has no recovery key!';
}
}
else {
$errors[] = "Player or account of player <b>" . escapeHtml($nick) . "</b> doesn't exist.";
}
if (!empty($errors)) {
$twig->display('error_box.html.twig', [
'errors' => $errors,
]);
}
$twig->display('account.back_button.html.twig', [
'new_line' => true,
'center' => true,
'action' => getLink('account/lost/recovery-key/step-2') . '?nick=' . urlencode($nick) . '&key=' . urlencode($key),
]);

View File

@@ -0,0 +1,26 @@
<?php
defined('MYAAC') or die('Direct access not allowed!');
csrfProtect();
$title = 'Lost Account';
$nick = $_REQUEST['nick'] ?? '';
$player = new OTS_Player();
$account = new OTS_Account();
$player->find($nick);
if($player->isLoaded()) {
$account = $player->getAccount();
}
if (ACTION == 'email') {
require __DIR__ . '/email/step-1.php';
}
else if (ACTION == 'recovery-key') {
require __DIR__ . '/recovery-key/step-1.php';
}
else {
$twig->display('account/lost/no-action.html.twig');
}

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';
@@ -96,12 +99,8 @@ if($email_new_time > 1)
}
}
$actions = array();
foreach($account_logged->getActionsLog(0, 1000) as $action) {
$actions[] = array('action' => $action['action'], 'date' => $action['date'], 'ip' => $action['ip'] != 0 ? long2ip($action['ip']) : inet_ntop($action['ipv6']));
}
$actions = $account_logged->getActionsLog(1000);
$players = array();
/** @var OTS_Players_List $account_players */
$account_players = $account_logged->getPlayersList();
$account_players->orderBy('id');
@@ -120,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

@@ -452,10 +452,8 @@ WHERE killers.death_id = '".$death['id']."' ORDER BY killers.final_hit DESC, kil
if($query->rowCount() > 0) {
echo 'Did you mean:<ul>';
foreach($query as $player) {
if(isset($player['promotion'])) {
if((int)$player['promotion'] > 0)
$player['vocation'] += ($player['promotion'] * $config['vocations_amount']);
}
$player['vocation'] = OTS_Toolbox::getVocationFromPromotion($player['vocation'], $player['promotion'] ?? 0);
echo '<li>' . getPlayerLink($player['name']) . ' (<small><strong>level ' . $player['level'] . ', ' . $config['vocations'][$player['vocation']] . '</strong></small>)</li>';
}
echo '</ul>';

View File

@@ -42,35 +42,12 @@ for($i = 0; $i < $threads_count['threads_count'] / setting('core.forum_threads_p
$links_to_pages .= '<b>'.($i + 1).' </b>';
}
echo '<a href="' . getLink('forum') . '">Boards</a> >> <b>'.escapeHtml($sections[$section_id]['name']).'</b>';
if($logged && (!$sections[$section_id]['closed'] || Forum::isModerator())) {
echo '<br /><br />
<a href="' . getLink('forum') . '?action=new_thread&section_id='.$section_id.'"><img src="images/forum/topic.gif" border="0" /></a>';
}
echo '<br /><br />Page: '.$links_to_pages.'<br />';
$last_threads = $db->query("SELECT `players`.`id` as `player_id`, `players`.`name`, `" . FORUM_TABLE_PREFIX . "forum`.`first_post`, `" . FORUM_TABLE_PREFIX . "forum`.`post_text`, `" . FORUM_TABLE_PREFIX . "forum`.`post_topic`, `" . FORUM_TABLE_PREFIX . "forum`.`id`, `" . FORUM_TABLE_PREFIX . "forum`.`last_post`, `" . FORUM_TABLE_PREFIX . "forum`.`replies`, `" . FORUM_TABLE_PREFIX . "forum`.`views`, `" . FORUM_TABLE_PREFIX . "forum`.`post_date` FROM `players`, `" . FORUM_TABLE_PREFIX . "forum` WHERE `players`.`id` = `" . FORUM_TABLE_PREFIX . "forum`.`author_guid` AND `" . FORUM_TABLE_PREFIX . "forum`.`section` = ".$section_id." AND `" . FORUM_TABLE_PREFIX . "forum`.`first_post` = `" . FORUM_TABLE_PREFIX . "forum`.`id` ORDER BY `" . FORUM_TABLE_PREFIX . "forum`.`last_post` DESC LIMIT ".setting('core.forum_threads_per_page')." OFFSET ".($_page * setting('core.forum_threads_per_page')))->fetchAll(PDO::FETCH_ASSOC);
if(isset($last_threads[0])) {
echo '<table width="100%">
<tr bgcolor="'.$config['vdarkborder'].'" align="center">
<td class="white">
<span style="font-size: 10px"><b>Thread</b></span></td>
<td><span style="font-size: 10px"><b>Thread Starter</b></span></td>
<td><span style="font-size: 10px"><b>Replies</b></span></td>
<td><span style="font-size: 10px"><b>Views</b></span></td>
<td><span style="font-size: 10px"><b>Last Post</b></span></td>
</tr>';
$threads = [];
if(count($last_threads) > 0) {
$player = new OTS_Player();
foreach($last_threads as $thread) {
echo '<tr bgcolor="' . getStyle($number_of_rows++) . '"><td>';
if(Forum::isModerator()) {
echo '<a href="' . getLink('forum') . '?action=move_thread&id=' . $thread['id'] . '" title="Move Thread"><img src="images/icons/arrow_right.gif"/></a>';
$twig->display('forum.remove_post.html.twig', ['post' => $thread]);
}
$player->load($thread['player_id']);
if(!$player->isLoaded()) {
throw new RuntimeException('Forum error: Player not loaded.');
@@ -79,28 +56,29 @@ if(isset($last_threads[0])) {
$player_account = $player->getAccount();
$canEditForum = $player_account->hasFlag(FLAG_CONTENT_FORUM) || $player_account->isAdmin();
echo '<a href="' . getForumThreadLink($thread['id']) . '">'.htmlspecialchars($thread['post_topic']). '</a><br /><small>'.($canEditForum ? substr(strip_tags($thread['post_text']), 0, 50) : htmlspecialchars(substr($thread['post_text'], 0, 50))).'...</small></td><td>' . getPlayerLink($thread['name']) . '</td><td>'.(int) $thread['replies'].'</td><td>'.(int) $thread['views'].'</td><td>';
$thread['link'] = getForumThreadLink($thread['id']);
$thread['post_shortened'] = ($canEditForum ? substr(strip_tags($thread['post_text']), 0, 50) : htmlspecialchars(substr($thread['post_text'], 0, 50)));
$thread['player'] = $player;
$thread['player_link'] = getPlayerLink($thread['name']);
if($thread['last_post'] > 0) {
$last_post = $db->query("SELECT `players`.`name`, `" . FORUM_TABLE_PREFIX . "forum`.`post_date` FROM `players`, `" . FORUM_TABLE_PREFIX . "forum` WHERE `" . FORUM_TABLE_PREFIX . "forum`.`first_post` = ".(int) $thread['id']." AND `players`.`id` = `" . FORUM_TABLE_PREFIX . "forum`.`author_guid` ORDER BY `post_date` DESC LIMIT 1")->fetch();
if(isset($last_post['name'])) {
echo date('d.m.y H:i:s', $last_post['post_date']) . '<br />by ' . getPlayerLink($last_post['name']);
}
else {
echo 'No posts.';
}
$last_post['player_link'] = getPlayerLink($last_post['name']);
$thread['latest_post'] = $last_post;
}
else {
echo date('d.m.y H:i:s', $thread['post_date']) . '<br />by ' . getPlayerLink($thread['name']);
}
echo '</td></tr>';
}
echo '</table>';
if($logged && (!$sections[$section_id]['closed'] || Forum::isModerator())) {
echo '<br /><a href="' . getLink('forum') . '?action=new_thread&section_id=' . $section_id . '"><img src="images/forum/topic.gif" border="0" /></a>';
$threads[] = $thread;
}
}
else {
echo '<h3>No threads in this board.</h3>';
}
$twig->display('forum.show_board.html.twig', [
'threads' => $threads,
'section_id' => $section_id,
'section_name' => $sections[$section_id]['name'],
'links_to_pages' => $links_to_pages,
'is_moderator' => Forum::isModerator(),
'closed' => $sections[$section_id]['closed'],
]);

View File

@@ -9,316 +9,25 @@
*/
use MyAAC\Cache\Cache;
use MyAAC\Models\Gallery as ModelsGallery;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Gallery';
$canEdit = hasFlag(FLAG_CONTENT_GALLERY) || superAdmin();
if($canEdit) {
if(function_exists('imagecreatefrompng')) {
if (!empty($action)) {
if ($action == 'delete' || $action == 'edit' || $action == 'hide' || $action == 'moveup' || $action == 'movedown')
$id = $_REQUEST['id'];
const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (isset($_REQUEST['comment']))
$comment = stripslashes($_REQUEST['comment']);
$images = Cache::remember('gallery', 5 * 60, function () {
$images = glob(BASE . GALLERY_DIR . '*.*');
if (isset($_REQUEST['image']))
$image = $_REQUEST['image'];
$images = array_filter($images, function ($image) {
$ext = pathinfo($image, PATHINFO_EXTENSION);
if (isset($_REQUEST['author']))
$author = $_REQUEST['author'];
return (in_array($ext, ALLOWED_EXTENSIONS) && !str_contains($image, '_thumb'));
});
$errors = array();
if ($action == 'add') {
if (Gallery::add($comment, $image, $author, $errors))
$comment = $image = $author = '';
} else if ($action == 'delete') {
Gallery::delete($id, $errors);
} else if ($action == 'edit') {
if (isset($id) && !isset($name)) {
$tmp = Gallery::get($id);
$comment = $tmp['comment'];
$image = $tmp['image'];
$author = $tmp['author'];
} else {
Gallery::update($id, $comment, $image, $author);
$action = $comment = $image = $author = '';
}
} else if ($action == 'hide') {
Gallery::toggleHide($id, $errors);
} else if ($action == 'moveup') {
Gallery::move($id, -1, $errors);
} else if ($action == 'movedown') {
Gallery::move($id, 1, $errors);
}
if (!empty($errors))
$twig->display('error_box.html.twig', array('errors' => $errors));
}
if(!isset($_GET['image'])) {
$twig->display('gallery.form.html.twig', array(
'link' => getLink('gallery/' . ($action == 'edit' ? 'edit' : 'add')),
'action' => $action,
'id' => isset($id) ? $id : null,
'comment' => isset($comment) ? $comment : null,
'image' => isset($image) ? $image : null,
'author' => isset($author) ? $author : null
));
}
}
else
echo 'You cannot edit/add gallery items as it seems your PHP installation doesnt have GD support enabled. Visit <a href="http://be2.php.net/manual/en/image.installation.php">PHP Manual</a> for more info.';
}
if(isset($_GET['image']))
{
$image = $db->query('SELECT * FROM `' . TABLE_PREFIX . 'gallery` WHERE `id` = ' . $db->quote($_GET['image']) . ' ORDER by `ordering` LIMIT 1;');
if($image->rowCount() == 1)
$image = $image->fetch();
else
{
echo 'Image with this id does not exists.';
return;
}
$previous_image = $db->query('SELECT `id` FROM `' . TABLE_PREFIX . 'gallery` WHERE `id` = ' . $db->quote($image['id'] - 1) . ' ORDER by `ordering`;');
if($previous_image->rowCount() == 1)
$previous_image = $previous_image->fetch();
else
$previous_image = NULL;
$next_image = $db->query('SELECT `id` FROM `' . TABLE_PREFIX . 'gallery` WHERE `id` = ' . $db->quote($image['id'] + 1) . ' ORDER by `ordering`;');
if($next_image->rowCount() == 1)
$next_image = $next_image->fetch();
else
$next_image = NULL;
$twig->display('gallery.get.html.twig', array(
'previous' => $previous_image ? $previous_image['id'] : null,
'next' => $next_image ? $next_image['id'] : null,
'image' => $image
));
return;
}
$images = Cache::remember('gallery_' . ($canEdit ? '1' : '0'), 60, function () use ($db, $canEdit) {
return $db->query('SELECT `id`, `comment`, `image`, `author`, `thumb`' .
($canEdit ? ', `hide`, `ordering`' : '') .
' FROM `' . TABLE_PREFIX . 'gallery`' .
(!$canEdit ? ' WHERE `hide` != 1' : '') .
' ORDER BY `ordering`;')->fetchAll(PDO::FETCH_ASSOC);
return array_map(function ($image) {
return basename($image);
}, $images);
});
$last = count($images);
if(!$last)
{
?>
There are no images added to gallery yet.
<?php
return;
}
$twig->display('gallery.html.twig', array(
$twig->display('gallery.html.twig', [
'images' => $images,
'last' => $last,
'canEdit' => $canEdit
));
class Gallery
{
static public function add($comment, $image, $author, &$errors)
{
global $db;
if(isset($comment[0]) && isset($image[0]) && isset($author[0]))
{
$query =
$db->query(
'SELECT `ordering`' .
' FROM `' . TABLE_PREFIX . 'gallery`' .
' ORDER BY `ordering`' . ' DESC LIMIT 1'
);
$ordering = 0;
if($query->rowCount() > 0) {
$query = $query->fetch();
$ordering = $query['ordering'] + 1;
}
$pathinfo = pathinfo($image);
$extension = strtolower($pathinfo['extension']);
$thumb_filename = GALLERY_DIR . $pathinfo['filename'] . '_thumb.' . $extension;
$filename = GALLERY_DIR . $pathinfo['filename'] . '.' . $extension;
if($db->insert(TABLE_PREFIX . 'gallery', array(
'comment' => $comment,
'image' => $filename, 'author' => $author,
'thumb' => $thumb_filename,
'ordering' => $ordering))) {
if(self::generateThumb($db->lastInsertId(), $image, $errors))
self::resize($image, 650, 500, $filename, $errors);
}
}
else
$errors[] = 'Please fill all inputs.';
return !count($errors);
}
static public function get($id) {
return ModelsGallery::find($id)->toArray();
}
static public function update($id, $comment, $image, $author) {
$pathinfo = pathinfo($image);
$extension = strtolower($pathinfo['extension']);
$filename = GALLERY_DIR . $pathinfo['filename'] . '.' . $extension;
if(ModelsGallery::where('id', $id)->update([
'comment' => $comment,
'image' => $filename,
'author' => $author
])) {
if(self::generateThumb($id, $image, $errors))
self::resize($image, 650, 500, $filename, $errors);
}
}
static public function delete($id, &$errors)
{
if(isset($id))
{
$row = ModelsGallery::find($id);
if($row)
if (!$row->delete()) {
$errors[] = 'Fail during delete Gallery';
}
else
$errors[] = 'Image with id ' . $id . ' does not exists.';
}
else
$errors[] = 'id not set';
return !count($errors);
}
static public function toggleHide($id, &$errors)
{
if(isset($id))
{
$row = ModelsGallery::find($id);
if($row) {
$row->hide = $row->hide == 1 ? 0 : 1;
if (!$row->save()) {
$errors[] = 'Fail during toggle hide Gallery';
}
} else
$errors[] = 'Image with id ' . $id . ' does not exists.';
}
else
$errors[] = 'id not set';
return !count($errors);
}
static public function move($id, $i, &$errors)
{
global $db;
$query = self::get($id);
if($query !== false)
{
$ordering = $query['ordering'] + $i;
$old_record = $db->select(TABLE_PREFIX . 'gallery', array('ordering' => $ordering));
if($old_record !== false) {
ModelsGallery::where('ordering', $ordering)->update([
'ordering' => $query['ordering'],
]);
}
ModelsGallery::where('id', $id)->update([
'ordering' => $ordering,
]);
}
else
$errors[] = 'Image with id ' . $id . ' does not exists.';
return !count($errors);
}
static public function resize($file, $new_width, $new_height, $new_file, &$errors)
{
$pathinfo = pathinfo($file);
$extension = strtolower($pathinfo['extension']);
switch ($extension)
{
case 'gif': // GIF
$image = imagecreatefromgif($file);
break;
case 'jpg': // JPEG
case 'jpeg':
$image = imagecreatefromjpeg($file);
break;
case 'png': // PNG
$image = imagecreatefrompng($file);
break;
default:
$errors[] = 'Unsupported file format.';
return false;
}
$width = imagesx($image);
$height = imagesy($image);
// create a new temporary image
$tmp_img = imagecreatetruecolor($new_width, $new_height);
// copy and resize old image into new image
imagecopyresized($tmp_img, $image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
// save thumbnail into a file
switch($extension)
{
case 'gif':
imagegif($tmp_img, $new_file);
break;
case 'jpg':
case 'jpeg':
imagejpeg($tmp_img, $new_file);
break;
case 'png':
imagepng($tmp_img, $new_file);
break;
}
return true;
}
static public function generateThumb($id, $file, &$errors)
{
$pathinfo = pathinfo($file);
$extension = strtolower($pathinfo['extension']);
$thumb_filename = GALLERY_DIR . $pathinfo['filename'] . '_thumb.' . $extension;
if(!self::resize($file, 170, 110, $thumb_filename, $errors))
return false;
if(isset($id))
{
$row = ModelsGallery::find($id);
if($row) {
$row->thumb = $thumb_filename;
$row->save();
} else
$errors[] = 'Image with id ' . $id . ' does not exists.';
}
else
$errors[] = 'id not set';
return !count($errors);
}
}
]);

View File

@@ -13,6 +13,7 @@ use MyAAC\Cache\Cache;
use MyAAC\Models\Player;
use MyAAC\Models\PlayerDeath;
use MyAAC\Models\PlayerKillers;
use MyAAC\Server\XML\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Highscores';
@@ -35,24 +36,20 @@ if(!is_numeric($page) || $page < 1 || $page > PHP_INT_MAX) {
$query = Player::query();
$configVocations = config('vocations');
$configVocationsAmount = config('vocations_amount');
$vocationId = null;
if($vocation !== 'all') {
foreach($configVocations as $id => $name) {
if(strtolower($name) == $vocation) {
$vocationId = $id;
$add_vocs = [$id];
$filterVocations = [$id];
if ($id !== 0) {
$i = $id + $configVocationsAmount;
while (isset($configVocations[$i])) {
$add_vocs[] = $i;
$i += $configVocationsAmount;
}
while($tmpVoc = Vocations::getPromoted($id)) {
$id = $tmpVoc;
$filterVocations[] = $tmpVoc;
}
$query->whereIn('players.vocation', $add_vocs);
$query->whereIn('players.vocation', $filterVocations);
break;
}
}
@@ -325,4 +322,5 @@ $twig->display('highscores.html.twig', [
'page' => $page,
'baseLink' => $baseLink,
'updatedAt' => $updatedAt,
'baseVocations' => Vocations::getBase(true),
]);

View File

@@ -12,6 +12,7 @@
use MyAAC\Cache\Cache;
use MyAAC\Models\ServerConfig;
use MyAAC\Models\ServerRecord;
use MyAAC\Server\XML\Vocations;
defined('MYAAC') or die('Direct access not allowed!');
$title = 'Who is online?';
@@ -56,15 +57,14 @@ $cached = Cache::remember("online_$order", setting('core.online_cache_ttl') * 60
$vocations = array_map(function ($name) {
return 0;
}, setting('core.vocations'));
}, config('vocations'));
if($db->hasTable('players_online')) // tfs 1.0
$playersOnline = $db->query('SELECT `accounts`.`country`, `players`.`name`, `players`.`level`, `players`.`vocation`' . $outfit . ', `' . $skull_time . '` as `skulltime`, `' . $skull_type . '` as `skull` FROM `accounts`, `players`, `players_online` WHERE `players`.`id` = `players_online`.`player_id` AND `accounts`.`id` = `players`.`account_id` ORDER BY ' . $orderSql);
else
$playersOnline = $db->query('SELECT `accounts`.`country`, `players`.`name`, `players`.`level`, `players`.`vocation`' . $outfit . ', ' . $promotion . ' `' . $skull_time . '` as `skulltime`, `' . $skull_type . '` as `skull` FROM `accounts`, `players` WHERE `players`.`online` > 0 AND `accounts`.`id` = `players`.`account_id` ORDER BY ' . $orderSql);
$settingVocations = setting('core.vocations');
$settingVocationsAmount = setting('core.vocations_amount');
$configVocations = config('vocations');
$players = [];
foreach($playersOnline as $player) {
@@ -81,22 +81,19 @@ $cached = Cache::remember("online_$order", setting('core.online_cache_ttl') * 60
}
}
if(isset($player['promotion'])) {
if((int)$player['promotion'] > 0)
$player['vocation'] += ($player['promotion'] * $settingVocationsAmount);
}
$player['vocation'] = OTS_Toolbox::getVocationFromPromotion($player['vocation'], $player['promotion'] ?? 0);
$players[] = array(
'name' => getPlayerLink($player['name']),
'player' => $player,
'level' => $player['level'],
'vocation' => $settingVocations[$player['vocation']],
'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'],
);
$vocations[($player['vocation'] > $settingVocationsAmount ? $player['vocation'] - $settingVocationsAmount : $player['vocation'])]++;
$vocations[Vocations::getOriginal($player['vocation'])]++;
}
$record = '';
@@ -142,6 +139,7 @@ $twig->display('online.html.twig', array(
'vocations' => $cached['vocations'],
'vocs' => $cached['vocations'], // deprecated, to be removed
'order' => $order,
'baseVocations' => Vocations::getBase(false),
));
// search bar

View File

@@ -8,6 +8,7 @@
* @link https://my-aac.org
*/
use MyAAC\Models\Menu;
use MyAAC\Models\Pages;
use MyAAC\Plugins;
@@ -331,7 +332,20 @@ else {
}
}
if (!$found) {
$tmpPageOriginal = $page;
$pagesWithDynamicPart = ['characters', 'forum', 'highscores', 'monsters'];
foreach ($pagesWithDynamicPart as $_page) {
if (str_contains($page, $_page)) {
$tmpPageOriginal = $_page;
}
}
$themeMenu = Menu::select(['name'])
->where('template', $template_name)
->where('link', $tmpPageOriginal)
->where('access', '>', $logged_access);
if (!$found || $themeMenu->count() >= 1) {
$page = '404';
$file = SYSTEM . 'pages/404.php';
}

View File

@@ -156,7 +156,7 @@ return [
'footer' => [
'name' => 'Custom Text',
'type' => 'textarea',
'desc' => 'Text displayed in the footer.<br/>For example: <i>' . escapeHtml('<br/>') . 'Your Server &copy; 2023. All rights reserved.</i>',
'desc' => 'Text displayed in the footer.<br/>For example: <i>' . escapeHtml('<br/>') . 'Your Server &copy; ' . date("Y") . '. All rights reserved.</i>',
'default' => '',
],
'footer_load_time' => [
@@ -219,7 +219,14 @@ return [
'cache_engine' => [
'name' => 'Cache Engine',
'type' => 'options',
'options' => ['auto' => 'Auto', 'file' => 'Files', 'apc' => 'APC', 'apcu' => 'APCu', 'disable' => 'Disable'],
'options' => [
'auto' => 'Auto',
'file' => 'Files',
'apc' => 'APC',
'apcu' => 'APCu',
'php' => 'PHP',
'disable' => 'Disable',
],
'desc' => 'Auto is most reasonable. It will detect the best cache engine',
'default' => 'auto',
'is_config' => true,
@@ -251,6 +258,28 @@ return [
'desc' => 'Allow MyAAC to report anonymous usage statistics to developers? The data is sent only once per 30 days and is fully confidential. It won\'t affect the performance of your website',
'default' => true,
],
[
'type' => 'section',
'title' => 'Custom HTML',
],
'html_head' => [
'name' => 'HTML Head',
'type' => 'textarea',
'desc' => escapeHtml('These scripts will be printed in the <head> section. Can be, for example, Google Analytics code.'),
'default' => '',
],
'html_body' => [
'name' => 'HTML Body',
'type' => 'textarea',
'desc' => escapeHtml('These scripts will be printed just below the opening <body> tag.'),
'default' => '',
],
'html_footer' => [
'name' => 'HTML Footer',
'type' => 'textarea',
'desc' => escapeHtml('These scripts will be printed above the closing </body> tag.'),
'default' => '',
],
[
'type' => 'category',
'title' => 'Game',
@@ -312,20 +341,31 @@ return [
},
],
],
/**
* @deprecated
* To be removed in v3.0
*/
'vocations_amount' => [
'name' => 'Vocations Amount',
'hidden' => true,
'type' => 'number',
'desc' => 'How much basic vocations your server got (without promotion)',
//'name' => 'Vocations Amount',
//'desc' => 'How many basic vocations your server got (without promotion)',
'default' => 4,
'callbacks' => [
'get' => function () {
return config('vocations_amount');
},
],
],
'vocations' => [
'name' => 'Vocation Names',
'hidden' => true,
'type' => 'textarea',
'desc' => 'Separated by comma. Must be in the same order as in vocations.xml, starting with id: 0.',
//'name' => 'Vocation Names',
//'desc' => 'Separated by comma. Must be in the same order as in vocations.xml, starting with id: 0.',
'default' => 'None, Sorcerer, Druid, Paladin, Knight, Master Sorcerer, Elder Druid,Royal Paladin, Elite Knight',
'callbacks' => [
'get' => function ($value) {
return array_map('trim', explode(',', $value));
'get' => function () {
return config('vocations');
},
],
],
@@ -1704,6 +1744,18 @@ Sent by MyAAC,<br/>
'account_login_ipban_protection', '=', 'true'
]
],
[
'type' => 'section',
'title' => 'Cooldowns',
],
'account_create_ip_block_cooldown' => [
'name' => 'Create Account IP Block Cooldown',
'type' => 'number',
'desc' => 'Block flooding create account per ip. If you still have a problem with account create spam - then its recommended to install the recaptcha plugin.' .
'<br/><strong>In minutes.</strong> 0 to disable.',
'default' => 10,
],
],
'callbacks' => [
'beforeSave' => function(&$settings, &$values) {

View File

@@ -13,8 +13,8 @@ namespace MyAAC\Cache;
class APC
{
private $prefix;
private $enabled;
private string $prefix;
private bool $enabled;
public function __construct($prefix = '')
{
@@ -22,14 +22,14 @@ class APC
$this->enabled = function_exists('apc_fetch');
}
public function set($key, $var, $ttl = 0)
public function set($key, $var, $ttl = 0): void
{
$key = $this->prefix . $key;
apc_delete($key);
apc_store($key, $var, $ttl);
}
public function get($key)
public function get($key): string
{
$tmp = '';
if ($this->fetch($this->prefix . $key, $tmp)) {
@@ -39,18 +39,15 @@ class APC
return '';
}
public function fetch($key, &$var)
{
public function fetch($key, &$var): bool {
return ($var = apc_fetch($this->prefix . $key)) !== false;
}
public function delete($key)
{
public function delete($key): void {
apc_delete($this->prefix . $key);
}
public function enabled()
{
public function enabled(): bool {
return $this->enabled;
}
}

View File

@@ -13,8 +13,8 @@ namespace MyAAC\Cache;
class APCu
{
private $prefix;
private $enabled;
private string $prefix;
private bool $enabled;
public function __construct($prefix = '')
{
@@ -22,14 +22,14 @@ class APCu
$this->enabled = function_exists('apcu_fetch');
}
public function set($key, $var, $ttl = 0)
public function set($key, $var, $ttl = 0): void
{
$key = $this->prefix . $key;
apcu_delete($key);
apcu_store($key, $var, $ttl);
}
public function get($key)
public function get($key): string
{
$tmp = '';
if ($this->fetch($this->prefix . $key, $tmp)) {
@@ -39,18 +39,15 @@ class APCu
return '';
}
public function fetch($key, &$var)
{
public function fetch($key, &$var): bool {
return ($var = apcu_fetch($this->prefix . $key)) !== false;
}
public function delete($key)
{
public function delete($key): void {
apcu_delete($this->prefix . $key);
}
public function enabled()
{
public function enabled(): bool {
return $this->enabled;
}
}

View File

@@ -47,35 +47,15 @@ class Cache
return self::$instance;
}
switch (strtolower($engine)) {
case 'apc':
self::$instance = new APC($prefix);
break;
case 'apcu':
self::$instance = new APCu($prefix);
break;
case 'xcache':
self::$instance = new XCache($prefix);
break;
case 'file':
self::$instance = new File($prefix, CACHE);
break;
case 'php':
self::$instance = new PHP($prefix, CACHE);
break;
case 'auto':
self::$instance = self::generateInstance(self::detect(), $prefix);
break;
default:
self::$instance = new self();
break;
}
self::$instance = match (strtolower($engine)) {
'apc' => new APC($prefix),
'apcu' => new APCu($prefix),
'xcache' => new XCache($prefix),
'file' => new File($prefix, CACHE),
'php' => new PHP($prefix, CACHE),
'auto' => self::generateInstance(self::detect(), $prefix),
default => new self(),
};
return self::$instance;
}
@@ -83,7 +63,7 @@ class Cache
/**
* @return string
*/
public static function detect()
public static function detect(): string
{
if (function_exists('apc_fetch'))
return 'apc';
@@ -98,8 +78,7 @@ class Cache
/**
* @return bool
*/
public function enabled()
{
public function enabled(): bool {
return false;
}

View File

@@ -12,18 +12,22 @@ namespace MyAAC\Cache;
class File
{
private $prefix;
private $dir;
private $enabled;
private string $prefix;
private string $dir;
private bool $enabled;
public function __construct($prefix = '', $dir = '')
{
$this->prefix = $prefix;
$this->dir = $dir;
ensureFolderExists($this->dir);
ensureIndexExists($this->dir);
$this->enabled = (file_exists($this->dir) && is_dir($this->dir) && is_writable($this->dir));
}
public function set($key, $var, $ttl = 0)
public function set($key, $var, $ttl = 0): void
{
$file = $this->_name($key);
file_put_contents($file, $var);
@@ -35,7 +39,7 @@ class File
touch($file, time() + $ttl);
}
public function get($key)
public function get($key): string
{
$tmp = '';
if ($this->fetch($key, $tmp)) {
@@ -45,7 +49,7 @@ class File
return '';
}
public function fetch($key, &$var)
public function fetch($key, &$var): bool
{
$file = $this->_name($key);
if (!file_exists($file) || filemtime($file) < time()) {
@@ -56,7 +60,7 @@ class File
return true;
}
public function delete($key)
public function delete($key): void
{
$file = $this->_name($key);
if (file_exists($file)) {
@@ -64,13 +68,11 @@ class File
}
}
public function enabled()
{
public function enabled(): bool {
return $this->enabled;
}
private function _name($key)
{
private function _name($key): string {
return sprintf('%s%s%s', $this->dir, $this->prefix, sha1($key));
}
}

View File

@@ -12,36 +12,37 @@ namespace MyAAC\Cache;
class PHP
{
private $prefix;
private $dir;
private $enabled;
private string $prefix;
private string $dir;
private bool $enabled;
public function __construct($prefix = '', $dir = '')
{
$this->prefix = $prefix;
$this->dir = $dir;
$this->enabled = (file_exists($this->dir) && is_dir($this->dir) && is_writable($this->dir));
}
public function set($key, $var, $ttl = 0)
{
$var = var_export($var, true);
ensureFolderExists($this->dir);
ensureIndexExists($this->dir);
// Write to temp file first to ensure atomicity
$tmp = $this->dir . "tmp_$key." . uniqid('', true) . '.tmp';
file_put_contents($tmp, '<?php $var = ' . $var . ';', LOCK_EX);
$this->enabled = (file_exists($this->dir) && is_dir($this->dir) && is_writable($this->dir));
}
$file = $this->_name($key);
rename($tmp, $file);
public function set($key, $var, $ttl = 0): void
{
$var = var_export($var, true);
if ($ttl === 0) {
$ttl = 365 * 24 * 60 * 60; // 365 days
}
touch($file, time() + $ttl);
$expires = time() + $ttl;
// Write to temp file first to ensure atomicity
$tmp = $this->dir . "tmp_$key." . uniqid('', true) . '.tmp';
file_put_contents($tmp, "<?php return ['expires' => $expires, 'var' => $var];", LOCK_EX);
$file = $this->_name($key);
rename($tmp, $file);
}
public function get($key)
@@ -54,19 +55,23 @@ class PHP
return '';
}
public function fetch($key, &$var)
public function fetch($key, &$var): bool
{
$file = $this->_name($key);
if (!file_exists($file) || filemtime($file) < time()) {
if (!file_exists($file)) {
return false;
}
@include $file;
$var = isset($var) ? $var : null;
$content = include $file;
if (!isset($content) || $content['expires'] < time()) {
return false;
}
$var = $content['var'];
return true;
}
public function delete($key)
public function delete($key): void
{
$file = $this->_name($key);
if (file_exists($file)) {
@@ -74,13 +79,11 @@ class PHP
}
}
public function enabled()
{
public function enabled(): bool {
return $this->enabled;
}
private function _name($key)
{
private function _name($key): string {
return sprintf('%s%s%s', $this->dir, $this->prefix, sha1($key) . '.php');
}
}

View File

@@ -13,8 +13,8 @@ namespace MyAAC\Cache;
class XCache
{
private $prefix;
private $enabled;
private string $prefix;
private bool $enabled;
public function __construct($prefix = '')
{
@@ -22,14 +22,14 @@ class XCache
$this->enabled = function_exists('xcache_get') && ini_get('xcache.var_size');
}
public function set($key, $var, $ttl = 0)
public function set($key, $var, $ttl = 0): void
{
$key = $this->prefix . $key;
xcache_unset($key);
xcache_set($key, $var, $ttl);
}
public function get($key)
public function get($key): string
{
$tmp = '';
if ($this->fetch($this->prefix . $key, $tmp)) {
@@ -39,7 +39,7 @@ class XCache
return '';
}
public function fetch($key, &$var)
public function fetch($key, &$var): bool
{
$key = $this->prefix . $key;
if (!xcache_isset($key)) {
@@ -50,13 +50,11 @@ class XCache
return true;
}
public function delete($key)
{
public function delete($key): void {
xcache_unset($this->prefix . $key);
}
public function enabled()
{
public function enabled(): bool {
return $this->enabled;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace MyAAC\Commands;
use MyAAC\Plugins;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class GiveAdminCommand extends Command
{
protected function configure(): void
{
$this->setName('give:admin')
->setDescription('This command adds super admin privileges to selected user')
->addArgument('account', InputArgument::REQUIRED, 'Account E-Mail, name or id');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
require SYSTEM . 'init.php';
$io = new SymfonyStyle($input, $output);
$account = new \OTS_Account();
$accountParam = $input->getArgument('account');
if (str_contains($accountParam, '@')) {
$account->findByEMail($accountParam);
}
else {
if (USE_ACCOUNT_NAME || USE_ACCOUNT_NUMBER) {
$account->find($accountParam);
}
else {
$account->load($accountParam);
}
}
if (!$account->isLoaded()) {
$io->error('Cannot find account mit supplied parameter: ' . $accountParam);
return self::FAILURE;
}
$account->setCustomField('web_flags', 3);
$io->success('Successfully added admin privileges to ' . $accountParam . ' (E-Mail: ' . $account->getEMail() . ')');
return self::SUCCESS;
}
}

View File

@@ -149,7 +149,10 @@ class CreateCharacter
if($db->hasColumn('players', 'direction'))
$player->setDirection($playerSample->getDirection());
$player->setConditions($playerSample->getConditions());
if($db->hasColumn('players', 'conditions')) {
$player->setConditions($playerSample->getConditions());
}
$rank = $playerSample->getRank();
if($rank->isLoaded()) {
$player->setRank($playerSample->getRank());
@@ -183,7 +186,11 @@ class CreateCharacter
$player->setLookHead($playerSample->getLookHead());
$player->setLookLegs($playerSample->getLookLegs());
$player->setLookType($playerSample->getLookType());
$player->setCap($playerSample->getCap());
if($db->hasColumn('players', 'cap')) {
$player->setCap($playerSample->getCap());
}
$player->setBalance(0);
$player->setPosX(0);
$player->setPosY(0);

View File

@@ -9,6 +9,6 @@ class AccountAction extends Model {
public $timestamps = false;
protected $fillable = ['account_id', 'ip', 'ipv6', 'date', 'action'];
protected $fillable = ['account_id', 'ip', 'date', 'action'];
}

View File

@@ -0,0 +1,18 @@
<?php
namespace MyAAC\Models;
use Illuminate\Database\Eloquent\Model;
class AccountBan extends Model {
protected $table = TABLE_PREFIX . 'account_bans';
public $timestamps = false;
protected $fillable = [
'account_id',
'reason', 'banned_at',
'expires_at', 'banned_by'
];
}

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

@@ -1,18 +0,0 @@
<?php
namespace MyAAC\Models;
use Illuminate\Database\Eloquent\Model;
class Gallery extends Model {
protected $table = TABLE_PREFIX . 'gallery';
public $timestamps = false;
protected $fillable = [
'comment', 'image', 'thumb',
'author', 'ordering', 'hide',
];
}

View File

@@ -9,6 +9,6 @@ class Menu extends Model {
public $timestamps = false;
protected $fillable = ['template', 'name', 'link', 'blank', 'color', 'category', 'ordering', 'enabled'];
protected $fillable = ['template', 'name', 'link', 'access', 'blank', 'color', 'category', 'ordering', 'enabled'];
}

View File

@@ -23,6 +23,8 @@ class Player extends Model {
public $timestamps = false;
protected $guarded = [];
protected $casts = [
'worldid' => 'integer',
'sex' => 'integer',
@@ -46,14 +48,8 @@ class Player extends Model {
});
}
public function getVocationNameAttribute()
{
$vocation = $this->vocation;
if (isset($this->promotion) && $this->promotion > 0) {
$vocation += ($this->promotion * setting('core.vocations_amount'));
}
return config('vocations')[$vocation] ?? 'Unknown';
public function getVocationNameAttribute() {
return \OTS_Toolbox::getVocationName($this->vocation, $this->promotion ?? 0);
}
public function getIsDeletedAttribute()

View File

@@ -0,0 +1,101 @@
<?php
namespace MyAAC\Server\XML;
use MyAAC\Cache\Cache;
class Vocations
{
private static array $vocations;
private static array $vocationsFrom;
public function __construct()
{
$cached = Cache::remember('vocations', 10 * 60, function () {
$this->load();
$from = $this->getFrom();
$amount = 0;
foreach ($from as $vocId => $fromVocation) {
if ($vocId != 0 && $vocId == $fromVocation) {
$amount++;
}
}
return ['vocations' => $this->get(), 'vocationsFrom' => $from, 'amount' => $amount];
});
self::$vocations = $cached['vocations'];
self::$vocationsFrom = $cached['vocationsFrom'];
config(['vocations', self::$vocations]);
config(['vocations_amount', $cached['amount']]);
}
public function load(): void
{
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') . 'vocations.xml';
}
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($vocationsXML->getElementsByTagName('vocation') as $vocation) {
$id = $vocation->getAttribute('id');
self::$vocations[$id] = $vocation->getAttribute('name');
$fromVocation = (int) $vocation->getAttribute('fromvoc');
self::$vocationsFrom[$id] = $fromVocation;
}
}
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 {
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

@@ -152,9 +152,10 @@ class Settings implements \ArrayAccess
}
if ($setting['type'] === 'category') {
$title = strtolower(str_replace(' ', '', $setting['title']));
?>
<li class="nav-item">
<a class="nav-link<?= ($i === 0 ? ' active' : ''); ?>" id="home-tab-<?= $i++; ?>" data-toggle="tab" href="#tab-<?= str_replace(' ', '', $setting['title']); ?>" type="button"><?= $setting['title']; ?></a>
<a class="nav-link<?= ($i === 0 ? ' active' : ''); ?>" id="home-tab-<?= $i++; ?>" data-toggle="tab" href="#tab-<?= $title; ?>" type="button"><?= $setting['title']; ?></a>
</li>
<?php
}
@@ -175,8 +176,10 @@ class Settings implements \ArrayAccess
if ($j++ !== 0) { // close previous category
echo '</tbody></table></div>';
}
$title = strtolower(str_replace(' ', '', $setting['title']));
?>
<div class="tab-pane fade show<?= ($j === 1 ? ' active' : ''); ?>" id="tab-<?= str_replace(' ', '', $setting['title']); ?>">
<div class="tab-pane fade show<?= ($j === 1 ? ' active' : ''); ?>" id="tab-<?= $title; ?>">
<?php
continue;
}

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

@@ -69,6 +69,15 @@ 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);
define('HOOK_ACCOUNT_LOST_RECOVERY_KEY_STEP_2_AFTER_CHARACTER', ++$i);
define('HOOK_ACCOUNT_LOST_RECOVERY_KEY_STEP_2_AFTER_EMAIL', ++$i);
define('HOOK_ACCOUNT_LOST_RECOVERY_KEY_STEP_2_AFTER_PASSWORD', ++$i);
define('HOOK_ACCOUNT_LOST_RECOVERY_KEY_STEP_2_AFTER_PASSWORD_REPEAT', ++$i);
define('HOOK_ACCOUNT_LOST_RECOVERY_KEY_STEP_3_POST', ++$i);
define('HOOK_ACCOUNT_CREATE_CHARACTER_AFTER', ++$i);
define('HOOK_ACCOUNT_CREATE_CHARACTER_BEFORE_FIRST_TABLE', ++$i);
define('HOOK_ACCOUNT_CREATE_CHARACTER_BEFORE_VOCATIONS', ++$i);

View File

@@ -146,10 +146,10 @@ if($twig_loader) {
function get_template_menus(): array
{
global $template_name;
global $template_name, $logged_access;
$result = Cache::remember('template_menus_' . $template_name, 10 * 60, function () use ($template_name) {
$result = Menu::select(['name', 'link', 'blank', 'color', 'category'])
$result = Menu::select(['name', 'link', 'access', 'blank', 'color', 'category'])
->where('template', $template_name)
->orderBy('category')
->orderBy('ordering')
@@ -163,6 +163,10 @@ function get_template_menus(): array
$menus = [];
foreach($result as $menu) {
if ($menu['access'] > $logged_access) {
continue;
}
if (empty($menu['link'])) {
$menu['link'] = 'news';
}

View File

@@ -1,7 +1,26 @@
{% if new_line is defined and new_line %}
<br/>
{% endif %}
<form action="{% if action is not defined %}{{ getLink('account/manage') }}{% else %}{{ action }}{% endif %}" method="post">
{{ csrf() }}
{{ include('buttons.back.html.twig') }}
</form>
{% set _center = false %}
{% if center is defined and center %}
{% set _center = true %}
{% endif %}
{% if _center %}
<table border="0" cellspacing="1" cellpadding="4" width="100%">
<tbody>
<tr>
<td align="center">
{% endif %}
<form action="{% if action is not defined %}{{ getLink('account/manage') }}{% else %}{{ action }}{% endif %}" method="post">
{{ csrf() }}
{{ include('buttons.back.html.twig') }}
</form>
{% if _center %}
</td>
</tr>
</tbody>
</table>
{% endif %}

View File

@@ -1,36 +0,0 @@
The Lost Account Interface can help you to get back your account name and password. Please enter your character name and select what you want to do.<br/>
<form action="{{ getLink('account/lost') }}?action=step1" method="post">
{{ csrf() }}
<input type="hidden" name="character" value="">
<table cellspacing="1" cellpadding="4" border="0" width="100%">
<tr>
<td bgcolor="{{ config.vdarkborder }}" class="white"><b>Please enter your character name</b></td>
</tr>
<tr>
<td bgcolor="{{ config.darkborder }}">
<input type="text" name="nick" size="40" autofocus/><br>
</td>
</tr>
</table>
<table cellspacing="1" cellpadding="4" border="0" width="100%">
<tr>
<td bgcolor="{{ config.vdarkborder }}" class="white"><b>What do you want?</b></td>
</tr>
<tr>
<td bgcolor="{{ config.darkborder }}">
<input type="radio" name="action_type" id="action_type_email" value="email">
<label for="action_type_email"> Send me new password and my account name to account e-mail adress.</label><br/>
<input type=radio name="action_type" id="action_type_key" value="reckey">
<label for="action_type_key"> I got <b>recovery key</b> and want set new password and e-mail adress to my account.</label><br/>
</td>
</tr>
</table>
<br/>
<table cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td align="center">
{{ include('buttons.submit.html.twig') }}
</td>
</tr>
</table>
</form>

View File

@@ -1,10 +0,0 @@
Please select action.<br/>
<table cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td align="center">
<a href="{{ getLink('account/lost') }}" border="0">
{{ include('buttons.back.html.twig') }}
</a>
</td>
</tr>
</table>

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