Files
TravianZ/GameEngine/Profile.php
T

468 lines
15 KiB
PHP
Executable File

<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename Profile.php ##
## Filename: Account.php ##
## Developed by: Dzoki ##
## Refactored by: Shadow (cata7007@gmail.com) ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
## URLs: https://travianz.org ##
## https://github.com/Shadowss/TravianZ ##
## ##
## Refactor notes (2026): ##
## - Incremental optimization (no logic changes) ##
## - Reduced repeated calls / minor optimizations ##
## - Added lightweight per-request caching ##
## - Improved readability + structure ##
## - PHP 7+ compatible ##
#################################################################################
class Profile {
/**
* Simple in-request cache (prevents duplicate DB calls inside same request)
*/
private static $cache = [
'vac_check' => [],
'villages' => []
];
public function procProfile($post) {
global $session;
if (isset($post['ft'])) {
switch ($post['ft']) {
case "p1":
$this->updateProfile($post);
break;
case "p2":
$this->updatePreferences($post);
break;
case "p3":
$this->updateAccount($post);
break;
case "p4":
$this->setvactionmode($post);
break;
}
}
if (isset($post['s']) && $post['s'] == 4) {
$this->gpack($post);
}
}
/**
* Preferences form (ft=p2): persist all per-user preferences.
* Stores the checkbox prefs (auto-completion v1-v3, large map, report
* filter v4-v6), the time preference (timezone/tformat) and the game
* language. Columns match struct.sql + Session seeding. Some of these are
* stored only for now and applied in-game incrementally (issue #198).
*/
private function updatePreferences($post) {
global $database, $session;
// Checkbox preferences -> 0/1 (unchecked boxes are absent from POST).
$v1 = empty($post['v1']) ? 0 : 1;
$v2 = empty($post['v2']) ? 0 : 1;
$v3 = empty($post['v3']) ? 0 : 1;
$map = empty($post['map']) ? 0 : 1;
$v4 = empty($post['v4']) ? 0 : 1;
$v5 = empty($post['v5']) ? 0 : 1;
$v6 = empty($post['v6']) ? 0 : 1;
// Time preference: timezone index + date format (0..3).
$timezone = isset($post['timezone']) ? (int)$post['timezone'] : 1;
$tformat = isset($post['tformat']) ? (int)$post['tformat'] : 0;
if ($tformat < 0 || $tformat > 3) {
$tformat = 0;
}
$database->query(
"UPDATE " . TB_PREFIX . "users SET " .
"v1=$v1, v2=$v2, v3=$v3, map=$map, v4=$v4, v5=$v5, v6=$v6, " .
"timezone=$timezone, tformat=$tformat " .
"WHERE id=" . (int)$session->uid
);
// Invalidate the 30s session user-cache (see Session::PopulateVar) so the
// reloaded page reflects the new values immediately, without a re-login.
$cacheKeyUser = 'cache_user_' . ($_SESSION['username'] ?? '');
unset($_SESSION[$cacheKeyUser]);
// Game language.
$allowed = ['en', 'fr', 'it', 'ro', 'zh'];
if (!empty($post['lang'])) {
$lang = strtolower(trim($post['lang']));
if (in_array($lang, $allowed, true)) {
$database->updateUserField($session->uid, "lang", $lang, 1);
$_SESSION['lang'] = $lang;
$session->userinfo['lang'] = $lang;
}
}
// Direct links table (add / update / delete the player's custom links).
$this->updateLinks($post);
header("Location: spieler.php?s=2");
exit;
}
/**
* Direct links (ft=p2): add / update / delete the player's custom menu
* links from the preferences form. This logic used to live in
* Templates/Profile/preference.tpl, but since procProfile() now intercepts
* the ft=p2 POST and exits, the template was never reached and links could
* no longer be saved (issue #204). Mirrors the form's nr / id / linkname /
* linkziel row fields.
*/
private function updateLinks($post) {
global $database, $session;
$uid = (int)$session->uid;
$links = [];
// Group the row fields (nr0, id0, linkname0, linkziel0, ...) by index.
foreach ($post as $key => $value) {
if (!is_string($value)) {
continue;
}
$value = trim($value);
if (strpos($key, 'linkname') === 0) {
$links[substr($key, 8)]['name'] = $value;
} elseif (strpos($key, 'linkziel') === 0) {
$links[substr($key, 8)]['url'] = $value;
} elseif (strpos($key, 'nr') === 0) {
$links[substr($key, 2)]['nr'] = (int)$value;
} elseif (strpos($key, 'id') === 0) {
$links[substr($key, 2)]['id'] = (int)$value;
}
}
foreach ($links as $link) {
$nr = isset($link['nr']) ? (int)$link['nr'] : 0;
$id = isset($link['id']) ? (int)$link['id'] : 0;
// --- REMOVE XSS ---
$name_raw = trim($link['name'] ?? '');
$url_raw = trim($link['url'] ?? '');
// name : without HTML, maximum 30 characters (as in the game), filtered through RemoveXSS
$name = $database->RemoveXSS(mb_substr(strip_tags($name_raw), 0, 30));
// url: accepts only http/https, max 120 characters, filtered through RemoveXSS
$url = '';
if ($url_raw !== '' && preg_match('#^https?://#i', $url_raw)) {
$url = $database->RemoveXSS(mb_substr($url_raw, 0, 120));
}
// SQL: escape for SQL (keeping the current TravianZ style used in TravianZ)
$name_sql = mysqli_real_escape_string($database->dblink, $name);
$url_sql = mysqli_real_escape_string($database->dblink, $url);
if ($nr !== 0 && $name !== '' && $url !== '' && $id === 0) {
// New link.
mysqli_query(
$database->dblink,
"INSERT INTO `" . TB_PREFIX . "links` (`userid`, `name`, `url`, `pos`) " .
"VALUES ($uid, '$name_sql', '$url_sql', $nr)"
);
} elseif ($nr !== 0 && $name !== '' && $url !== '' && $id > 0) {
// Update existing link (ownership enforced in the WHERE clause).
mysqli_query(
$database->dblink,
"UPDATE `" . TB_PREFIX . "links` SET `name`='$name_sql', `url`='$url_sql', `pos`=$nr " .
"WHERE `id`=$id AND `userid`=$uid"
);
} elseif ($nr === 0 && $name === '' && $url === '' && $id > 0) {
// Emptied row -> delete (ownership enforced in the WHERE clause).
mysqli_query(
$database->dblink,
"DELETE FROM `" . TB_PREFIX . "links` WHERE `id`=$id AND `userid`=$uid"
);
}
}
}
public function procSpecial($get) {
if (isset($get['e'])) {
switch ($get['e']) {
case 2:
$this->removeMeSit($get);
break;
case 3:
$this->removeSitter($get);
break;
case 4:
$this->cancelDeleting($get);
break;
}
}
}
/**
* Update player profile + village names
*/
private function updateProfile($post) {
global $database, $session;
$birthday = $post['jahr'] . '-' . $post['monat'] . '-' . $post['tag'];
$birthday = preg_match('/^\d{4}-\d{1,2}-\d{1,2}$/', $birthday) ? $birthday : '0';
$mw = (int)($post['mw'] ?? 0);
// BUG-3: store the location/descriptions raw. submitProfile() uses a prepared
// statement (SQL-safe) and every display site escapes with htmlspecialchars()
// before rendering BBCode, so RemoveXSS() here only double-escaped the data
// (baked-in backslashes from its SQL escape + literal &quot; entities).
$ort = trim($post['ort'] ?? '');
$be1 = trim($post['be1'] ?? ''); // right description
$be2 = trim($post['be2'] ?? ''); // left description
$database->submitProfile($session->uid, $mw, $ort, $birthday, $be2, $be1);
// Cache villages
if (!isset(self::$cache['villages'][$session->uid])) {
self::$cache['villages'][$session->uid] = $database->getProfileVillages($session->uid);
}
$varray = self::$cache['villages'][$session->uid];
$cnt = count($varray);
for ($i = 0; $i < $cnt; $i++) {
if (!isset($post['dname' . $i])) continue;
$newName = trim($post['dname' . $i]);
if ($newName === '') continue;
$database->setVillageName($varray[$i]['wref'], $newName);
}
// Invalidate the 30s session user-cache (see Session::PopulateVar) so the
// saved description/birthday/etc. show up immediately on the edit form and
// header, without waiting for the cache to expire (issue #250).
unset($_SESSION['cache_user_' . ($_SESSION['username'] ?? '')]);
header("Location: spieler.php?uid=" . $session->uid);
exit;
}
/**
* Gpack settings
*/
private function gpack($post) {
global $database, $session;
$database->gpack(
$database->RemoveXSS($session->uid),
$database->RemoveXSS($post['custom_url'])
);
// Invalidate the 30s session user-cache (see Session::PopulateVar) so the
// new graphics pack applies immediately, without a re-login.
unset($_SESSION['cache_user_' . ($_SESSION['username'] ?? '')]);
header("Location: spieler.php?uid=" . $session->uid);
exit;
}
/**
* Vacation mode activation
*/
private function setvactionmode($post) {
global $database, $session;
if (isset($post['vac']) && $post['vac'] && isset($post['vac_days']) && $post['vac_days'] >= 2 && $post['vac_days'] <= 14) {
$uid = $session->uid;
// Cache check per request (avoid duplicate heavy checks)
if (!isset(self::$cache['vac_check'][$uid])) {
self::$cache['vac_check'][$uid] = $database->checkVacationRequirements($uid);
}
$check = self::$cache['vac_check'][$uid];
if ($check !== true) {
$messages = [
"TROOPS_MOVING" => "You still have troops moving",
"INCOMING_TROOPS" => "You have incoming troops",
"REINFORCEMENTS" => "You have reinforcements on your villages",
"WW" => "You own a Wonder of the World",
"ARTEFACTS" => "You own artefacts",
"PROTECTION" => "You are still under beginner protection",
"PRISONERS_IN" => "No units trapped in your traps",
"PRISONERS_OUT" => "No units in enemy traps",
"MARKET" => "Marketplace transport active",
"ACCOUNT_DELETION" => "Account is scheduled for deletion"
];
$output = "";
foreach ($check as $err) {
$output .= (isset($messages[$err]) ? $messages[$err] : $err) . "<br>";
}
$_SESSION['vac_error'] = $output;
header("Location: spieler.php?s=5");
exit;
}
// OK -> enter vacation mode
unset($_SESSION['wid']);
$database->setvacmode($uid, $post['vac_days']);
$database->activeModify(addslashes($session->username), 1);
$database->UpdateOnline("logout");
$session->Logout();
header("Location: login.php");
exit;
} else {
$_SESSION['vac_error'] = "Vacation days must be between 2 and 14";
header("Location: spieler.php?s=5");
exit;
}
}
/**
* Update account settings (password, email, sitter, deletion)
*/
private function updateAccount($post) {
global $database, $session, $form;
// Password change
if (!empty($post['pw1']) && !empty($post['pw2']) && !empty($post['pw3'])) {
if ($post['pw2'] == $post['pw3']) {
if ($database->login($session->username, $post['pw1'])) {
$database->updateUserField(
$session->uid,
"password",
password_hash($post['pw2'], PASSWORD_BCRYPT, ['cost' => 12]),
1
);
} else {
$form->addError("pw", LOGIN_PW_ERROR);
}
} else {
$form->addError("pw", PASS_MISMATCH);
}
}
// Email change
if (!empty($post['email_alt']) && !empty($post['email_neu'])) {
if ($post['email_alt'] == $session->userinfo['email']) {
$database->updateUserField($session->uid, "email", $post['email_neu'], 1);
} else {
$form->addError("email", EMAIL_ERROR);
}
}
// Delete request cancel
if (!empty($post['del_pw']) && !empty($post['del'])) {
if (password_verify($post['del_pw'], $session->userinfo['password'])) {
$database->setDeleting($session->uid, 0);
} else {
$form->addError("del", PASS_MISMATCH);
}
}
// Sitter assignment
if (!empty($post['v1'])) {
$sitid = $database->getUserField($post['v1'], "id", 1);
if ($sitid == $session->userinfo['sit1'] || $sitid == $session->userinfo['sit2']) {
$form->addError("sit", SIT_ERROR);
} else if ($sitid != $session->uid) {
if ($session->userinfo['sit1'] == 0) {
$database->updateUserField($session->uid, "sit1", $sitid, 1);
} else if ($session->userinfo['sit2'] == 0) {
$database->updateUserField($session->uid, "sit2", $sitid, 1);
}
}
}
// Persist errors if any
if ($form->returnErrors() > 0) {
$_SESSION['errorarray'] = $form->getErrors();
$_SESSION['valuearray'] = $_POST;
}
// Invalidate the 30s session user-cache (see Session::PopulateVar) so the
// updated account fields (email, sitters, password) are reflected
// immediately, without a re-login.
unset($_SESSION['cache_user_' . ($_SESSION['username'] ?? '')]);
header("Location: spieler.php?s=3");
exit;
}
/**
* Remove sitter
*/
private function removeSitter($get) {
global $database, $session;
if ($get['a'] == $session->checker) {
if ($session->userinfo['sit' . $get['type']] == $get['id']) {
$database->updateUserField($session->uid, "sit" . $get['type'], 0, 1);
// Invalidate the 30s session user-cache (see Session::PopulateVar) so the
// removed sitter disappears immediately, without a re-login.
unset($_SESSION['cache_user_' . ($_SESSION['username'] ?? '')]);
}
$session->changeChecker();
}
header("Location: spieler.php?s=" . $get['s']);
exit;
}
/**
* Cancel account deletion
*/
private function cancelDeleting($get) {
global $database, $session;
$database->setDeleting($session->uid, 1);
header("Location: spieler.php?s=" . $get['s']);
exit;
}
/**
* Remove me as sitter
*/
private function removeMeSit($get) {
global $database, $session;
if ($get['a'] == $session->checker) {
$database->removeMeSit($get['id'], $session->uid);
$session->changeChecker();
}
header("Location: spieler.php?s=" . $get['s']);
exit;
}
}
$profile = new Profile;
?>