Dragon-Knight/src/actions/admin.php

567 lines
16 KiB
PHP

<?php
// admin.php :: primary administration script.
namespace Admin;
use Router;
use SQLite3Result;
function register_routes(Router $r): Router
{
if (user() !== false && user()->authlevel === 1) {
$r->get('/admin', 'Admin\donothing');
$r->form('/admin/main', 'Admin\primary');
$r->get('/admin/items', 'Admin\items');
$r->form('/admin/items/:id', 'Admin\edit_item');
$r->get('/admin/drops', 'Admin\drops');
$r->form('/admin/drops/:id', 'Admin\edit_drop');
$r->get('/admin/towns', 'Admin\towns');
$r->form('/admin/towns/:id', 'Admin\edit_town');
$r->get('/admin/monsters', 'Admin\monsters');
$r->form('/admin/monsters/:id', 'Admin\edit_monster');
$r->get('/admin/levels', 'Admin\levels');
$r->post('/admin/levels', 'Admin\edit_level');
$r->get('/admin/spells', 'Admin\spells');
$r->form('/admin/spells/:id', 'Admin\edit_spell');
$r->get('/admin/users', 'Admin\users');
$r->form('/admin/users/:id', 'Admin\edit_user');
$r->form('/admin/news', 'Admin\add_news');
}
return $r;
}
/**
* Home page for the admin panel.
*/
function donothing(): string
{
page_title('Admin');
return <<<HTML
Welcome to the administration section. Use the links on the left bar to control and edit various
elements of the game.
<br><br>
Please note that the control panel has been created mostly as a shortcut for certain individual settings. It is
meant for use primarily with editing one thing at a time. If you need to completely replace an entire table
(say, to replace all stock monsters with your own new ones), it is suggested that you use a more in-depth
database tool such as <a href="https://sqlitebrowser.org/" target="_new">SQLite Browser</a>.
<br><br>
Also, you should be aware that certain portions of the DK code are dependent on the formatting of certain
database results (for example, the special attributes on item drops). While I have attempted to point these out
throughout the admin script, you should definitely pay attention and be careful when editing some fields,
because mistakes in the database content may result in script errors or your game breaking completely.
HTML;
}
/**
* Main settings that get written to .env
*/
function primary(): string
{
if (is_post()) {
$form = validate($_POST, [
'gamename' => ['alphanum-spaces'],
'gamesize' => ['int', 'min:5'],
'class1name' => ['alpha-spaces'],
'class2name' => ['alpha-spaces'],
'class3name' => ['alpha-spaces'],
'gameopen' => ['bool'],
'verifyemail' => ['bool'],
'shownews' => ['bool'],
'showonline' => ['bool'],
'showbabble' => ['bool']
]);
if ($form['valid']) {
$form = $form['data'];
if (($form['gamesize'] % 5) != 0) exit('Map size must be divisible by five.');
// @todo
// write changes to .env
$page = 'Main settings updated.';
} else {
$error_list = ul_from_validate_errors($form['errors']);
$page = <<<HTML
<b>Errors:</b><br>
<div style="color: red;">{$error_list}</div><br>
Please go back and try again.
HTML;
}
} else {
$page = render('admin/main_settings');
}
page_title('Admin: Main Settings');
return $page;
}
/**
* Show the full list of items that can be edited.
*/
function items(): string
{
$items = db()->query('SELECT * FROM items ORDER BY id;');
$page = "<h2>Edit Items</h2>Click an item's name or ID to edit it.<br><br>\n";
$page .= build_bulk_table($items, 'name', '/admin/items');
page_title('Admin: Items');
return $page;
}
/**
* Shows the form for editing an item via GET, processes edits via POST
*/
function edit_item(int $id): string
{
$item = get_item($id);
if (is_post()) {
$page = handle_edit_form($id, 'items', validate($_POST, [
'name' => [],
'type' => ['int', 'in:1,2,3'],
'buycost' => ['int', 'min:0'],
'attribute' => ['int', 'min:0'],
'special' => ['default:X']
]));
} else {
$page = render('admin/edit_item', ['item' => $item]);
}
page_title('Admin: Editing '.$item['name']);
return $page;
}
/**
* Show the full list of drops that can be edited
*/
function drops()
{
$drops = db()->query('SELECT * FROM drops ORDER BY id;');
$page = "<h2>Edit Drops</h2>Click an item's name to edit it.<br><br>\n";
$page .= build_bulk_table($drops, 'name', '/admin/drops');
page_title('Admin: Drops');
return $page;
}
/**
* Show the form to edit drops via GET, process those edits via POST
*/
function edit_drop(int $id): string
{
$drop = get_drop($id);
if (is_post()) {
$page = handle_edit_form($id, 'drops', validate($_POST, [
'name' => [],
'mlevel' => ['int', 'min:1'],
'attribute1' => [],
'attribute2' => ['default:X'],
]));
} else {
$page = render('admin/edit_drop', ['drop' => $drop]);
}
page_title('Admin: Editing '.$drop['name']);
return $page;
}
/**
* Generate the list of towns that can be edited.
*/
function towns(): string
{
$towns = db()->query('SELECT * FROM towns ORDER BY id;');
$page = "<h2>Edit Towns</h2>Click an town's name or ID to edit it.<br><br>\n";
$page .= build_bulk_table($towns, 'name', '/admin/towns');
page_title('Admin: Towns');
return $page;
}
/**
* Save any changes to the town made.
*/
function edit_town(int $id): string
{
$town = get_town_by_id($id);
if (is_post()) {
$page = handle_edit_form($id, 'towns', validate($_POST, [
'name' => [],
'latitude' => ['int', 'min:0', 'max:'.env('game_size')],
'longitude' => ['int', 'min:0', 'max:'.env('game_size')],
'innprice' => ['int', 'min:0'],
'mapprice' => ['int', 'min:0'],
'travelpoints' => ['int', 'min:0'],
'itemslist' => ['optional']
]));
} else {
$page = render('admin/edit_town', ['town' => $town]);
}
page_title('Admin: Editing '.$town['name']);
return $page;
}
/**
* List the monsters available to edit.
*/
function monsters()
{
$max_level = db()->query('SELECT level FROM monsters ORDER BY level DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['level'];
$monsters = db()->query('SELECT * FROM monsters ORDER BY id;');
$page = "<h2>Edit Monsters</h2>";
if ((env('game_size') / 5) !== $max_level) {
$page .= "<span class=\"highlight\">Note:</span> Your highest monster level does not match with your entered map size. Highest monster level should be ".(env('game_size') / 5).", yours is $max_level. Please fix this before opening the game to the public.<br>";
} else {
$page .= "Monster level and map size match. No further actions are required for map compatibility.<br>";
}
$page .= "Click an monster's name or ID to edit it.<br><br>\n";
$page .= build_bulk_table($monsters, 'name', '/admin/monsters');
page_title('Admin: Monsters');
return $page;
}
/**
* Handle the actual editing of the monster.
*/
function edit_monster(int $id): string
{
$monster = get_monster($id);
if (is_post()) {
$page = handle_edit_form($id, 'monsters', validate($_POST, [
'name' => [],
'maxhp' => ['int', 'min:1'],
'maxdam' => ['int', 'min:0'],
'armor' => ['int', 'min:0'],
'level' => ['int', 'min:1'],
'maxexp' => ['int', 'min:0'],
'maxgold' => ['int', 'min:0'],
'immune' => ['in:0,1,2']
]));
} else {
$page = render('admin/edit_monster', ['monster' => $monster]);
}
page_title('Admin: Editing '.$monster['name']);
return $page;
}
/**
* List all spells available to edit.
*/
function spells(): string
{
$page = "<h2>Edit Spells</h2>Click an spell's name to edit it.<br><br>\n";
$spells = db()->query('SELECT * FROM spells ORDER BY id;');
$page .= build_bulk_table($spells, 'name', '/admin/spells');
page_title('Admin: Spells');
return $page;
}
/**
* Handle the editing of an individual spell.
*/
function edit_spell(int $id): string
{
$spell = get_spell($id);
if (is_post()) {
$page = handle_edit_form($id, 'spells', validate($_POST, [
'name' => [],
'mp' => ['int', 'min:0'],
'attribute' => ['int', 'min:0'],
'type' => ['in:1,2,3,4,5']
]));
} else {
$page = render('admin/edit_spell', ['spell' => $spell]);
}
page_title('Admin: Editing '.$spell['name']);
return $page;
}
/**
* List all editable levels.
*/
function levels(): string
{
$max_level = db()->query('SELECT id FROM levels ORDER BY id DESC LIMIT 1;')->fetchArray(SQLITE3_ASSOC)['id'];
$page = <<<HTML
<h2>Edit Levels</h2>
Select a level number from the dropdown box to edit it.<br><br>
<form action="/admin/levels" method="post" hx-post="/admin/levels" hx-target="#main">
<select name="level">
HTML;
for ($i = 2; $i < $max_level; $i++) $page .= "<option value=\"$i\">$i</option>\n";
$page .= <<<HTML
</select>
<button type="submit">Edit</button>
</form>
HTML;
page_title('Admin: Levels');
return $page;
}
/**
* Handle the editing of a level.
*/
function edit_level()
{
if (!isset($_POST['level'])) return 'No level to edit.';
$id = $_POST['level'];
$level = db()->query('SELECT * FROM levels WHERE id=? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
if (is_post() && isset($_POST['save'])) {
unset($_POST['save']);
unset($_POST['level']);
$page = handle_edit_form($id, 'levels', validate($_POST, [
'1_exp' => ['int', 'min:0'],
'1_hp' => ['int', 'min:0'],
'1_mp' => ['int', 'min:0'],
'1_tp' => ['int', 'min:0'],
'1_strength' => ['int', 'min:0'],
'1_dexterity' => ['int', 'min:0'],
'1_spells' => ['int', 'min:0'],
'2_exp' => ['int', 'min:0'],
'2_hp' => ['int', 'min:0'],
'2_mp' => ['int', 'min:0'],
'2_tp' => ['int', 'min:0'],
'2_strength' => ['int', 'min:0'],
'2_dexterity' => ['int', 'min:0'],
'2_spells' => ['int', 'min:0'],
'3_exp' => ['int', 'min:0'],
'3_hp' => ['int', 'min:0'],
'3_mp' => ['int', 'min:0'],
'3_tp' => ['int', 'min:0'],
'3_strength' => ['int', 'min:0'],
'3_dexterity' => ['int', 'min:0'],
'3_spells' => ['int', 'min:0']
]), 'Level <b>'.$id.'</b> updated.');
} else {
$page = render('admin/edit_level', ['level' => $level]);
}
page_title('Admin: Editing Level '.$id);
return $page;
}
function users()
{
$users = db()->query('SELECT * FROM users ORDER BY id;');
$page = "<h2>Edit Users</h2>Click a username or ID to edit the account.<br><br><div class=\"table-wrapper\">";
$page .= build_bulk_table($users, 'username', '/admin/users');
page_title('Admin: Users');
return $page . '</div>';
}
/**
* Handle editing a user.
*/
function edit_user(int $id): string
{
$user = db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$id])->fetchArray(SQLITE3_ASSOC);
if (is_post()) {
$form = validate($_POST, [
'username' => ['length:3-18', 'alpha-spaces', 'unique:users,username'],
'verify' => [],
'authlevel' => ['int'],
'email' => ['email', 'unique:users,email'],
'charclass' => ['in:1,2,3'],
'latitude' => ['int', 'min:0', 'max:'.env('game_size')],
'longitude' => ['int', 'min:0', 'max:'.env('game_size')],
'currentaction' => [],
'currentfight' => ['int'],
'currentmonster' => ['int'],
'currentmonsterhp' => ['int'],
'currentmonstersleep' => ['int'],
'currentmonsterimmune' => ['int'],
'currentuberdamage' => ['int'],
'currentuberdefense' => ['int'],
'currenthp' => ['int', 'min:0'],
'currentmp' => ['int', 'min:0'],
'currenttp' => ['int', 'min:0'],
'maxhp' => ['int', 'min:1'],
'maxmp' => ['int', 'min:1'],
'maxtp' => ['int', 'min:1'],
'level' => ['int', 'min:1'],
'gold' => ['int', 'min:0'],
'experience' => ['int', 'min:0'],
'goldbonus' => ['int'],
'expbonus' => ['int'],
'strength' => ['int'],
'dexterity' => ['int'],
'attackpower' => ['int'],
'defensepower' => ['int'],
'weaponid' => ['int'],
'armorid' => ['int'],
'shieldid' => ['int'],
'slot1id' => ['int'],
'slot2id' => ['int'],
'slot3id' => ['int'],
'weaponname' => ['default:None'],
'armorname' => ['default:None'],
'shieldname' => ['default:None'],
'slot1name' => ['default:None'],
'slot2name' => ['default:None'],
'slot3name' => ['default:None'],
'dropcode' => ['int', 'min:0', 'default:0'],
'spells' => ['optional'],
'towns' => ['optional']
]);
if ($form['valid']) {
save_data_row('users', $form['data'], $id);
$page = 'User <b>'.$user['username'].'</b> updated.';
} else {
$error_list = ul_from_validate_errors($form['errors']);
$page = <<<HTML
<b>Errors:</b><br>
<div style="color: red;">{$error_list}</div><br>
Please go back and try again.
HTML;
}
} else {
$page = render('admin/edit_user', ['user' => $user]);
}
page_title('Admin: Editing '.$user['username']);
return $page;
}
/**
* Handling adding news posts.
*/
function add_news()
{
if (is_post()) {
$c = trim($_POST['content'] ?? '');
$errors = [];
if (empty($c)) $errors[] = "Content is required.";
if (count($errors) === 0) {
db()->query('INSERT INTO news (author, content) VALUES (?, ?);', [user()->username, $c]);
$page = 'News post added.';
} else {
$error_list = implode('<br>', $errors);
$page = "<b>Errors:</b><br><div style=\"color:red;\">$error_list</div><br>Please go back and try again.";
}
} else {
$page = <<<HTML
<h2>Add a News Post</h2>
<form action="/admin/news" method="post" hx-post="/admin/news" hx-target="#main">
Type your post below and then click Submit to add it.<br>
<textarea name="content" rows="5" cols="50"></textarea><br>
<button type="submit">Submit</button>
<button type="reset">Clear</button>
</form>
HTML;
}
page_title('Admin: Add News');
return $page;
}
/**
* Build an HTML table containing all columns and rows of a given data structure. Takes a SQLiteResult3 of a SELECT
* query.
*/
function build_bulk_table(SQLite3Result $query_data, string $edit_column, string $edit_link): string
{
$data = [];
$data_count = 0;
while ($row = $query_data->fetchArray(SQLITE3_ASSOC)) $data[$data_count++] = $row;
if ($data_count === 0) return 'No data.';
$columns = array_diff(array_keys($data[0]), ['password']);
$html_parts = [
'<table><colgroup>',
str_repeat('<col>', count($columns)),
'</colgroup><thead><tr>'
];
foreach ($columns as $column) {
$html_parts[] = '<th>' .
make_safe($column === 'id' ? 'ID' : ucfirst($column)) .
'</th>';
}
$html_parts[] = '</tr></thead><tbody>';
$is_edit_column = array_flip(['id', $edit_column]);
foreach ($data as $row) {
$html_parts[] = '<tr>';
foreach ($columns as $column) {
$name = make_safe($row[$column]);
$html_parts[] = isset($is_edit_column[$column])
? "<td><a href=\"{$edit_link}/{$row['id']}\" hx-get=\"{$edit_link}/{$row['id']}\" hx-target=\"#main\">{$name}</a></td>"
: "<td>{$name}</td>";
}
$html_parts[] = '</tr>';
}
$html_parts[] = '</tbody></table>';
return implode('', $html_parts);
}
/**
* Save a row of data to it's table from the data supplied.
*/
function save_data_row(string $table, array $data, int $id): SQLite3Result|false
{
$data = array_filter($data, fn($value) => $value !== null && $value !== '');
if (empty($data)) return false;
$fields = implode(',', array_map(fn($key) => "`$key`=?", array_keys($data)));
$values = array_values($data);
$values[] = $id;
return db()->query("UPDATE $table SET $fields WHERE id=?", $values);
}
/**
* Handle the result of a generic edit form.
*/
function handle_edit_form(int $id, string $table, array $form, string $updated_message = ''): string
{
if ($form['valid']) {
save_data_row($table, $form['data'], $id);
$page = $updated_message ?: '<b>'.$form['data']['name'].'</b> updated.';
} else {
$error_list = ul_from_validate_errors($form['errors']);
$page = <<<HTML
<b>Errors:</b><br>
<div style="color: red;">{$error_list}</div><br>
Please go back and try again.
HTML;
}
return $page;
}