editcharacter.php

main 284 lines · 10.9 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Character Management page: page.php?p=editcharacter&character=<name>
4 *
5 * One character's actions (comment, gender, visibility, rename, delete) on a
6 * single dedicated page instead of a dropdown+submit combo on myaccount.php.
7 * Reuses the exact same server-side rules as myaccount.php's own switch, just
8 * scoped to the character validated below instead of a posted selector.
9 */
10
11protect_page();
12
13$char_name = getValue($_GET['character'] ?? null);
14$char_id = $char_name !== false ? user_character_id($char_name) : false;
15
16if ($char_name === false || $char_id === false || (int)user_character_account_id($char_name) !== (int)$session_user_id) {
17 echo '<h1>' . t('page.not_found') . '</h1>';
18 return;
19}
20
21$errors = [];
22$notice = null;
23$deleted = false;
24
25if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && !empty($_POST['action'])) {
26 if (!Token::isValid($_POST['token'] ?? null)) {
27 exit();
28 }
29 $action = getValue($_POST['action'] ?? null);
30
31 switch ($action) {
32 case 'toggle_hide':
33 $hide = (user_character_hide($char_name) == 1 ? 0 : 1);
34 user_character_set_hide($char_id, $hide);
35 break;
36
37 case 'change_gender':
38 if (!user_is_online_10($char_id)) {
39 $tickets = shop_account_gender_tickets($session_user_id);
40 $tickets = is_array($tickets) ? $tickets : array();
41 if (!empty($tickets) || $config['free_sex_change'] == true) {
42 $infinite = false;
43 $tks = 0;
44 foreach ($tickets as $ticket) {
45 if ($ticket['count'] == 0) $infinite = true;
46 else if ((int)$ticket['count'] > 0 && $infinite === false) $tks += (int)$ticket['count'];
47 }
48 if ($infinite === true) $tks = 0;
49 $dbid = isset($tickets[0]['id']) ? (int)$tickets[0]['id'] : 0;
50 if ($dbid > 0 && $tickets[0]['count'] > 1) {
51 $tks--;
52 shop_update_row_count($dbid, ((int)$tickets[0]['count'] - 1));
53 } else if ($dbid > 0 && $tickets[0]['count'] == 1) {
54 shop_delete_row_order($dbid);
55 $tks--;
56 }
57 user_character_change_gender($char_name);
58 $notice = t('acc.gender_changed', ['name' => $char_name]);
59 if ($tks > 0) $notice .= ' ' . t('acc.gender_tickets_left', ['n' => $tks]);
60 else if ($infinite !== true) $notice .= ' ' . t('acc.gender_tickets_out');
61 } else {
62 $errors[] = t('acc.no_gender_tickets');
63 }
64 } else {
65 $errors[] = t('acc.must_be_offline');
66 }
67 break;
68
69 case 'update_comment':
70 user_update_comment($char_id, getValue($_POST['comment'] ?? null));
71 $notice = t('acc.comment_updated');
72 break;
73
74 case 'change_name':
75 $newname = isset($_POST['newName']) ? getValue($_POST['newName'] ?? null) : '';
76
77 if (user_is_online_10($char_id)) {
78 $errors[] = t('acc.must_be_offline');
79 }
80
81 $order = db()->fetchOne(
82 "SELECT `id`, `account_id` FROM `znote_shop_orders` WHERE `type` = 4 AND `account_id` = ? LIMIT 1;",
83 [(int)$session_user_id]
84 );
85 if ($order === false) {
86 $errors[] = t('acc.no_name_tickets');
87 }
88
89 $validated = validate_name($newname);
90 if ($validated === false) {
91 $errors[] = t('acc.name_max_words');
92 } else {
93 $newname = $validated;
94 if (empty($newname)) {
95 $errors[] = t('acc.name_required');
96 } else if (user_character_exist($newname) !== false) {
97 $errors[] = t('acc.name_taken');
98 } else if (!preg_match("/^[a-zA-Z_ ]+$/", $newname)) {
99 $errors[] = t('acc.name_letters');
100 } else if (strlen($newname) < $config['minL'] || strlen($newname) > $config['maxL']) {
101 $errors[] = t('acc.name_length', ['min' => $config['minL'], 'max' => $config['maxL']]);
102 } else if (!ctype_upper($newname[0])) {
103 $errors[] = t('acc.name_capital');
104 }
105
106 $resname = explode(' ', (string)($_POST['newName'] ?? ''));
107 foreach ($resname as $res) {
108 if (in_array(strtolower($res), $config['invalidNameTags'])) {
109 $errors[] = t('reg.restricted_word');
110 } else if (strlen($res) == 1) {
111 $errors[] = t('reg.words_too_short');
112 }
113 }
114 }
115
116 if (empty($errors) && !empty($newname)) {
117 $db = db();
118 if (!$db->beginTransaction()) {
119 $errors[] = t('acc.sync_failed');
120 } else {
121 $ok = $db->execute("UPDATE `players` SET `name` = ? WHERE `id` = ? LIMIT 1;", [$newname, $char_id]);
122 $ok = $ok && $db->execute("DELETE FROM `znote_shop_orders` WHERE `id` = ? LIMIT 1;", [(int)$order['id']]);
123 if ($ok) {
124 $db->commit();
125 $char_name = $newname;
126 $notice = t('acc.name_changed', ['name' => $newname]);
127 } else {
128 $db->rollback();
129 $errors[] = t('acc.sync_failed');
130 }
131 }
132 }
133 break;
134
135 case 'delete_character':
136 if (!user_is_online_10($char_id)) {
137 if (guild_leader_gid($char_id) === false) {
138 user_delete_character_soft($char_id);
139 $deleted = true;
140 } else {
141 $errors[] = t('acc.is_guild_leader');
142 }
143 } else {
144 $errors[] = t('acc.must_be_offline');
145 }
146 break;
147 }
148}
149
150$char = null;
151$gender = null;
152$comment_data = null;
153if (!$deleted) {
154 $rows = user_character_list($session_user_id);
155 if (is_array($rows)) {
156 foreach ($rows as $row) {
157 if ($row['name'] === $char_name) {
158 $char = $row;
159 break;
160 }
161 }
162 }
163 $gender = user_character_data($char_id, 'sex');
164 $comment_data = user_znote_character_data($char_id, 'comment');
165}
166
167$nameTicketCount = (int)(db()->fetchOne(
168 "SELECT COUNT(*) AS `c` FROM `znote_shop_orders` WHERE `type` = 4 AND `account_id` = ?;",
169 [(int)$session_user_id]
170)['c'] ?? 0);
171
172$genderTickets = shop_account_gender_tickets($session_user_id);
173$genderTickets = is_array($genderTickets) ? $genderTickets : array();
174$genderTicketInfinite = false;
175$genderTicketCount = 0;
176foreach ($genderTickets as $ticket) {
177 if ($ticket['count'] == 0) $genderTicketInfinite = true;
178 else if ((int)$ticket['count'] > 0 && $genderTicketInfinite === false) $genderTicketCount += (int)$ticket['count'];
179}
180?>
181<div class="znx-acct">
182
183<div class="znx-editchar-back"><a href="myaccount.php">&laquo; <?= t_default('acc.back_to_account', 'Back to My Account') ?></a></div>
184
185<?php if (!empty($errors)): ?>
186 <div class="znx-acct-info znx-editchar-notice znx-editchar-notice--error"><?php echo output_errors($errors); ?></div>
187<?php endif; ?>
188<?php if ($notice): ?>
189 <div class="znx-acct-info znx-editchar-notice znx-editchar-notice--ok"><?php echo $notice; ?></div>
190<?php endif; ?>
191
192<?php if ($deleted): ?>
193 <div class="znx-acct-head"><?= htmlspecialchars($char_name, ENT_QUOTES, 'UTF-8') ?></div>
194 <div class="znx-acct-info">
195 <p><?= t_default('acc.delete_scheduled', 'This character is scheduled for deletion. You can cancel it from your account page.') ?></p>
196 <a href="myaccount.php" class="znx-acct-btn"><?= t_default('acc.back_to_account', 'Back to My Account') ?></a>
197 </div>
198<?php elseif ($char !== null): ?>
199
200 <div class="znx-acct-head"><?= htmlspecialchars($char_name, ENT_QUOTES, 'UTF-8') ?> &mdash; <?= t_default('acc.character_management', 'Character Management') ?></div>
201 <div class="znx-acct-info znx-editchar-info">
202 <div class="znx-editchar-info__row"><span><?= mb_strtoupper(t('common.vocation')) ?></span><strong><?= htmlspecialchars((string)$char['vocation'], ENT_QUOTES, 'UTF-8') ?></strong></div>
203 <div class="znx-editchar-info__row"><span><?= mb_strtoupper(t('common.level')) ?></span><strong><?= (int)$char['level'] ?></strong></div>
204 <div class="znx-editchar-info__row"><span><?= mb_strtoupper(t('common.town')) ?></span><strong><?= htmlspecialchars((string)$char['town_id'], ENT_QUOTES, 'UTF-8') ?></strong></div>
205 </div>
206
207 <div class="znx-acct-head"><?= t('acc.change_name') ?></div>
208 <?php if ($nameTicketCount > 0): ?>
209 <form action="" method="post" class="znx-editchar-box">
210 <input type="hidden" name="action" value="change_name">
211 <?php Token::create(); ?>
212 <div class="znx-editchar-ticket-note"><?= t_default('acc.tickets_remaining', 'Remaining:') ?> <strong><?= $nameTicketCount ?></strong></div>
213 <div class="znx-acct-toolbar">
214 <div class="znx-acct-toolbar__field">
215 <input type="text" name="newName" placeholder="<?= htmlspecialchars(t('common.new_name_placeholder'), ENT_QUOTES, 'UTF-8') ?>" class="znx-acct-select">
216 </div>
217 <div class="znx-acct-toolbar__submit"><button type="submit" class="znx-acct-btn"><?= t('common.submit') ?></button></div>
218 </div>
219 </form>
220 <?php else: ?>
221 <div class="znx-editchar-box znx-editchar-box--muted"><?= t('acc.no_name_tickets') ?></div>
222 <?php endif; ?>
223
224 <div class="znx-acct-head"><?= t('acc.change_gender') ?></div>
225 <form action="" method="post" class="znx-editchar-box">
226 <input type="hidden" name="action" value="change_gender">
227 <?php Token::create(); ?>
228 <div class="znx-editchar-inline">
229 <span><?= t_default('acc.current_gender', 'Current gender:') ?> <strong><?= ($gender && (int)$gender['sex'] === 1) ? t_default('common.male', 'Male') : t_default('common.female', 'Female') ?></strong></span>
230 <button type="submit" class="znx-char-action"><?= t('acc.change_gender') ?></button>
231 </div>
232 <div class="znx-editchar-ticket-note">
233 <?php if ($config['free_sex_change'] == true): ?>
234 <?= t_default('acc.tickets_free', 'Free for this account.') ?>
235 <?php elseif ($genderTicketInfinite): ?>
236 <?= t_default('acc.tickets_unlimited', 'Unlimited gender change tickets.') ?>
237 <?php elseif ($genderTicketCount > 0): ?>
238 <?= t_default('acc.tickets_remaining', 'Remaining:') ?> <strong><?= $genderTicketCount ?></strong>
239 <?php else: ?>
240 <?= t('acc.no_gender_tickets') ?>
241 <?php endif; ?>
242 </div>
243 </form>
244
245 <div class="znx-acct-head"><?= t_default('acc.visibility', 'Visibility') ?></div>
246 <form action="" method="post" class="znx-editchar-box">
247 <input type="hidden" name="action" value="toggle_hide">
248 <?php Token::create(); ?>
249 <div class="znx-editchar-inline">
250 <span><?= t_default('acc.current_status', 'Current status:') ?> <strong><?= htmlspecialchars(hide_char_to_name($char['hide_char']), ENT_QUOTES, 'UTF-8') ?></strong></span>
251 <button type="submit" class="znx-char-action"><?= t('acc.toggle_hide') ?></button>
252 </div>
253 </form>
254
255 <div class="znx-acct-head"><?= t('acc.change_comment') ?></div>
256 <form action="" method="post" class="znx-editchar-box">
257 <input type="hidden" name="action" value="update_comment">
258 <?php Token::create(); ?>
259 <textarea name="comment" class="znx-editchar-textarea" rows="6"><?php echo htmlspecialchars((string)($comment_data['comment'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></textarea>
260 <button type="submit" class="znx-acct-btn"><?= t('acc.update_comment') ?></button>
261 </form>
262
263 <div class="znx-acct-head"><?= t('acc.delete_char') ?></div>
264 <form action="" method="post" class="znx-editchar-box">
265 <input type="hidden" name="action" value="delete_character">
266 <?php Token::create(); ?>
267 <button type="submit" class="znx-char-action znx-char-action--danger needconfirmation"><?= t('acc.delete_char') ?></button>
268 </form>
269
270<?php endif; ?>
271
272</div>
273<script>
274 document.addEventListener('DOMContentLoaded', function () {
275 document.querySelectorAll('.needconfirmation').forEach(function (btn) {
276 btn.addEventListener('click', function (event) {
277 if (!confirm(<?= json_encode(t('acc.confirm_delete', ['name' => $char_name])) ?>)) {
278 event.preventDefault();
279 }
280 });
281 });
282 });
283</script>
284
Top