Issue #299: posting to an admin Mod (eg editBuildings.php) could show an
essentially blank page. The admin panel and the game share the same PHP
session, so a game logout (session_destroy) — or a mobile browser dropping the
session cookie / serving a cached form with a stale token — wipes the admin
session. The Mod then stopped on a bare die('<h1>Access Denied</h1>') (or the
403 die() in csrf_verify()), which renders as a blank/broken page outside the
panel.
Add a shared admin_deny() helper in GameEngine/Admin/csrf.php that renders a
clean, self-contained, styled error page (with a "Return to Admin Panel" link)
and a no-store header, then exits. Wire it into csrf_verify() and replace every
bare "Access Denied" die() across the 42 admin Mods. Each Mod now loads
csrf.php at the top so admin_deny() is available before its first access check.
This is the presentation fix Shadow asked for ("we must receive an error not
blank page"). The deeper root cause (admin and game sharing one PHP session) is
left for a follow-up: giving the admin panel its own session cookie name.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bug fix: $oasisowned was fetched but never applied — this is the
"time to overflow" timer's own, independent recomputation of the
production rate (used only for display here), and it silently
excluded oasis bonuses entirely. Village::getCropProd()/getWoodProd()
etc. (which actually grow the stored resource in the DB on every
page load via processProduction()) do add a flat 25% per matching
oasis (Village::sortOasis()), so the real stockpile was growing
faster than this timer's denominator assumed — understating the
rate, and therefore overstating the time remaining. Mirrored here
with the same counting + application order (oasis bonus on the raw
field total, then the building bonus below on top of that), so this
rate matches the one actually used to fill the storage.
// Bug fix: RemoveXSS() calls htmlspecialchars() (&,<,>,",' -> entities).
// Every display site for these values ALREADY escapes correctly on output
// (links.tpl's safeHTML(), and preference.tpl's edit-row value=""), so
// encoding here too meant a saved "&" was stored as literal "&" text
// in the DB, then got escaped AGAIN on redisplay — surviving one level of
// browser entity-decoding as visible "&". Worse, it silently broke
// any saved link with a real query parameter after the first one (e.g.
// build.php?gid=16&t=99): the stored value no longer had a real "&"
// separator there, so "t" was never received as its own GET param.
// strip_tags() (for name) + mysqli_real_escape_string() (below, for SQL)
// are sufficient at save time; HTML-escaping belongs only at display time.
sendTroops() inlined ~65 lines deciding the catapult targets ctar1/ctar2: the
"Rivals great confusion" artefact lookup, the rally-point-level-driven list of
invalid target buildings, the troop/level eligibility rules and the Teuton
Brewery / artefact adjustments. Move that whole block into
resolveCatapultTargets(&$post, $data), which mutates $post['ctar1']/['ctar2'] by
reference exactly as before; sendTroops() now calls it before building the
attack. None of the block's locals were used afterwards. Behaviour-preserving.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Both branches of Hero() (single hero when !$all, full list when $all) computed
the same five derived stats (atk/di/dc/ob/db) and assembled a byte-identical
hero stat array from a getHero() entry plus its unit base data. Extract that
into buildHeroStats($hero, $herodata) and call it from both branches.
Behaviour-preserving.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Cases 1 to 4 of the procUnits() switch had a byte-identical body (send troops
when the rally-point form is submitted, otherwise load the unit form). Stack the
four case labels and keep a single shared body via switch fall-through.
Behaviour-preserving.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
upgradeSword() and upgradeArmour() were near-identical: the only differences
were the AB-tech key prefix ('b' vs 'a'), the building type whose level gates
the research (Smithy 12 vs Armoury 13) and the matching bid building data
($bid12 vs $bid13). Merge them into a single upgradeWeaponOrArmour($get, $type)
parameterised by the prefix, deriving the building type from it, and route both
procTechno() cases through it. Resolves the pre-existing //TODO. Behaviour-
preserving.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
sendMessage, massmessage and sysmessage are POSTed to directly, bypassing
admin.php's central csrf_verify(). Add csrf_verify() (after the admin access
check, via the shared GameEngine/Admin/csrf.php) and csrf_field() in their
forms (Newmessage.tpl, massmessage.tpl, sysmessage.tpl; the mass/sys templates
have both a prepare and an execute form).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The post-delete admin-log block referenced variables that were never defined
($admid/$adminID/$medalid/$uid), so on PHP 8.1+ (mysqli throws on error) the
malformed INSERT raised an uncaught mysqli_sql_exception → HTTP 500 after the
medal was already deleted. Use the correct ids ($admid from session, $uid from
POST), look up the target player's username (escaped), and redirect to the
sanitized $uid.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
editAli, delAli, medals, delallymedal, delallymedalbyaid, delallymedalbyweek
and deletemedalbyweek are POSTed to directly, bypassing admin.php's central
csrf_verify(). Add csrf_verify() (after the admin access check, via the shared
GameEngine/Admin/csrf.php) and csrf_field() in their forms (playermedals.tpl,
editAli.tpl, delAli.tpl, delmedal.tpl, allymedals.tpl, delallymedal.tpl).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The alliance/editAli/delAli pages are linked all over the admin panel
(?p=alliance&aid=, ?p=editAli, ?p=delAli) but were never in
admin_validated_page()'s whitelist, so admin.php fell back to search.tpl and
the pages never showed. Add them to the whitelist plus switch cases for the
breadcrumb (the templates resolve $aid/$alidata themselves from $_GET, like
editSitter/editPassword).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>