mirror of
https://github.com/Shadowss/TravianZ.git
synced 2026-10-21 00:25:17 +08:00
Fix activation checks across start-date boundaries (#395)
This commit is contained in:
@@ -223,7 +223,7 @@ class Account {
|
||||
global $database;
|
||||
|
||||
// ==================== VERIFICARE DATA DE START A SERVERULUI ====================
|
||||
if (START_DATE < date('d.m.Y') || (START_DATE === date('d.m.Y') && START_TIME <= date('H:i'))) {
|
||||
if (\App\Utils\DateTime::hasServerStarted(START_DATE, START_TIME)) {
|
||||
|
||||
// Caută codul de activare în tabela activate
|
||||
$code = trim($_POST['id'] ?? '');
|
||||
|
||||
@@ -45,10 +45,10 @@ if(isset($show)){
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php if(START_DATE > date('d.m.y') || START_DATE == date('d.m.y') && START_TIME <= date('H:i')){ ?>
|
||||
<?php if(!$serverHasStarted && $serverStartTimestamp !== null){ ?>
|
||||
<br/><center><big><?php echo TZ_ACTIVATION_AVAILBLE_IN; ?> </big></center>
|
||||
<script>
|
||||
TargetDate = "<?php echo START_DATE; ?> <?php echo START_TIME; ?>";
|
||||
TargetDate = <?php echo json_encode($serverStartTimestamp); ?>;
|
||||
CountActive = true;
|
||||
CountStepper = -1;
|
||||
LeadingZero = true;
|
||||
@@ -104,7 +104,7 @@ if (CountStepper == 0)
|
||||
CountActive = false;
|
||||
var SetTimeOutPeriod = (Math.abs(CountStepper)-1)*1000 + 990;
|
||||
putspan(BackColor, ForeColor);
|
||||
var dthen = new Date(TargetDate);
|
||||
var dthen = new Date(TargetDate * 1000);
|
||||
var dnow = new Date();
|
||||
if(CountStepper>0)
|
||||
ddiff = new Date(dnow-dthen);
|
||||
@@ -117,11 +117,11 @@ CountBack(gsecs);
|
||||
<?php }}else{ ?>
|
||||
<p>
|
||||
<?php
|
||||
if(START_DATE > date('d.m.y') || START_DATE == date('d.m.y') && START_TIME <= date('H:i')){
|
||||
if(!$serverHasStarted && $serverStartTimestamp !== null){
|
||||
?>
|
||||
<br/><center><big><?php echo TZ_ACTIVATION_AVAILBLE_IN; ?> </big></center>
|
||||
<script language="JavaScript">
|
||||
TargetDate = "<?php echo START_DATE; ?> <?php echo START_TIME; ?>";
|
||||
TargetDate = <?php echo json_encode($serverStartTimestamp); ?>;
|
||||
CountActive = true;
|
||||
CountStepper = -1;
|
||||
LeadingZero = true;
|
||||
@@ -177,7 +177,7 @@ if (CountStepper == 0)
|
||||
CountActive = false;
|
||||
var SetTimeOutPeriod = (Math.abs(CountStepper)-1)*1000 + 990;
|
||||
putspan(BackColor, ForeColor);
|
||||
var dthen = new Date(TargetDate);
|
||||
var dthen = new Date(TargetDate * 1000);
|
||||
var dnow = new Date();
|
||||
if(CountStepper>0)
|
||||
ddiff = new Date(dnow-dthen);
|
||||
@@ -188,7 +188,7 @@ CountBack(gsecs);
|
||||
|
||||
</script>
|
||||
<?php
|
||||
}else{ ?>
|
||||
}elseif($serverHasStarted){ ?>
|
||||
<div id="activation">
|
||||
<form action="activate.php" method="post">
|
||||
<p class="important">
|
||||
@@ -205,4 +205,4 @@ CountBack(gsecs);
|
||||
</div>
|
||||
|
||||
<?php }
|
||||
?>
|
||||
?>
|
||||
|
||||
+5
-2
@@ -25,6 +25,9 @@ use App\Utils\AccessLogger;
|
||||
include('GameEngine/Account.php');
|
||||
AccessLogger::logRequest();
|
||||
|
||||
$serverStartTimestamp = \App\Utils\DateTime::getServerStartTimestamp(START_DATE, START_TIME);
|
||||
$serverHasStarted = $serverStartTimestamp !== null && time() >= $serverStartTimestamp;
|
||||
|
||||
?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
@@ -56,7 +59,7 @@ AccessLogger::logRequest();
|
||||
<div id="content" class="activate">
|
||||
<?php
|
||||
|
||||
if(isset($_GET['e']) && (START_DATE < date('d.m.y') || START_DATE == date('d.m.y') && START_TIME <= date('H:i')))
|
||||
if(isset($_GET['e']) && $serverHasStarted)
|
||||
{
|
||||
switch($_GET['e'])
|
||||
{
|
||||
@@ -93,4 +96,4 @@ AccessLogger::logRequest();
|
||||
<?php include("Templates/footer.tpl"); ?>
|
||||
<div id="ce"></div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+52
-1
@@ -23,6 +23,57 @@ namespace App\Utils;
|
||||
*/
|
||||
class DateTime {
|
||||
|
||||
/**
|
||||
* Parse the configured game-world start using the server timezone.
|
||||
*
|
||||
* The installer writes START_DATE as d.m.Y and START_TIME as H:i. Parsing
|
||||
* them together avoids the month/year ordering bugs caused by comparing
|
||||
* the formatted values as strings.
|
||||
*
|
||||
* @return int|null Unix timestamp, or null when the configuration is invalid.
|
||||
*/
|
||||
public static function getServerStartTimestamp($startDate, $startTime, $timezone = null)
|
||||
{
|
||||
if (!$timezone instanceof \DateTimeZone) {
|
||||
$timezone = new \DateTimeZone(date_default_timezone_get());
|
||||
}
|
||||
|
||||
$value = trim((string) $startDate) . ' ' . trim((string) $startTime);
|
||||
|
||||
try {
|
||||
$start = \DateTimeImmutable::createFromFormat('!d.m.Y H:i', $value, $timezone);
|
||||
} catch (\ValueError $exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$errors = \DateTimeImmutable::getLastErrors();
|
||||
|
||||
if (
|
||||
$start === false
|
||||
|| ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))
|
||||
|| $start->format('d.m.Y H:i') !== $value
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $start->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true from the first second of the configured start minute.
|
||||
*/
|
||||
public static function hasServerStarted($startDate, $startTime, $now = null, $timezone = null)
|
||||
{
|
||||
$start = self::getServerStartTimestamp($startDate, $startTime, $timezone);
|
||||
if ($start === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$now = $now === null ? time() : (int) $now;
|
||||
|
||||
return $now >= $start;
|
||||
}
|
||||
|
||||
public static function getTimeFormat($time)
|
||||
{
|
||||
$min = 0;
|
||||
@@ -42,4 +93,4 @@ class DateTime {
|
||||
return $days ." day ".$hr."h ".$min."m ".$time."s";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../src/Utils/DateTime.php';
|
||||
|
||||
use App\Utils\DateTime as TravianDateTime;
|
||||
|
||||
function fail_server_start_test($message)
|
||||
{
|
||||
fwrite(STDERR, $message . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function test_timestamp($value, DateTimeZone $timezone)
|
||||
{
|
||||
return (new DateTimeImmutable($value, $timezone))->getTimestamp();
|
||||
}
|
||||
|
||||
$lisbon = new DateTimeZone('Europe/Lisbon');
|
||||
$utc = new DateTimeZone('UTC');
|
||||
$kathmandu = new DateTimeZone('Asia/Kathmandu');
|
||||
|
||||
$decisionCases = [
|
||||
['one second before', '20.09.2026', '12:00', '2026-09-20 11:59:59', false, $lisbon],
|
||||
['exact start second', '20.09.2026', '12:00', '2026-09-20 12:00:00', true, $lisbon],
|
||||
['inside start minute', '20.09.2026', '12:00', '2026-09-20 12:00:59', true, $lisbon],
|
||||
['one second after minute', '20.09.2026', '12:00', '2026-09-20 12:01:00', true, $lisbon],
|
||||
['earlier same day', '20.09.2026', '08:30', '2026-09-20 12:00:00', true, $lisbon],
|
||||
['later same day', '20.09.2026', '18:30', '2026-09-20 12:00:00', false, $lisbon],
|
||||
['previous day', '19.09.2026', '23:59', '2026-09-20 00:00:00', true, $lisbon],
|
||||
['next day', '21.09.2026', '00:00', '2026-09-20 23:59:59', false, $lisbon],
|
||||
['previous month', '25.08.2026', '12:00', '2026-09-20 12:00:00', true, $lisbon],
|
||||
['next month', '02.10.2026', '12:00', '2026-09-20 12:00:00', false, $lisbon],
|
||||
['previous year', '31.12.2025', '23:59', '2026-01-01 00:00:00', true, $lisbon],
|
||||
['next year', '01.01.2027', '00:00', '2026-12-31 23:59:59', false, $lisbon],
|
||||
['valid leap day exact', '29.02.2024', '00:00', '2024-02-29 00:00:00', true, $utc],
|
||||
['UTC boundary', '01.01.1970', '00:00', '1970-01-01 00:00:00', true, $utc],
|
||||
['positive offset before', '01.01.2026', '00:00', '2025-12-31 23:59:59', false, $kathmandu],
|
||||
['positive offset exact', '01.01.2026', '00:00', '2026-01-01 00:00:00', true, $kathmandu],
|
||||
];
|
||||
|
||||
foreach ($decisionCases as [$label, $date, $time, $now, $expected, $timezone]) {
|
||||
$actual = TravianDateTime::hasServerStarted(
|
||||
$date,
|
||||
$time,
|
||||
test_timestamp($now, $timezone),
|
||||
$timezone
|
||||
);
|
||||
|
||||
if ($actual !== $expected) {
|
||||
fail_server_start_test(
|
||||
$label . ': expected ' . var_export($expected, true)
|
||||
. ', got ' . var_export($actual, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$validParseCases = [
|
||||
['epoch', '01.01.1970', '00:00', $utc, 0],
|
||||
[
|
||||
'leap day',
|
||||
'29.02.2024',
|
||||
'23:59',
|
||||
$utc,
|
||||
test_timestamp('2024-02-29 23:59:00', $utc),
|
||||
],
|
||||
[
|
||||
'leap century',
|
||||
'29.02.2000',
|
||||
'12:00',
|
||||
$utc,
|
||||
test_timestamp('2000-02-29 12:00:00', $utc),
|
||||
],
|
||||
[
|
||||
'last minute of year',
|
||||
'31.12.2026',
|
||||
'23:59',
|
||||
$lisbon,
|
||||
test_timestamp('2026-12-31 23:59:00', $lisbon),
|
||||
],
|
||||
[
|
||||
'surrounding whitespace',
|
||||
' 20.09.2026 ',
|
||||
' 12:00 ',
|
||||
$lisbon,
|
||||
test_timestamp('2026-09-20 12:00:00', $lisbon),
|
||||
],
|
||||
[
|
||||
'non-hour timezone',
|
||||
'01.01.2026',
|
||||
'00:00',
|
||||
$kathmandu,
|
||||
test_timestamp('2026-01-01 00:00:00', $kathmandu),
|
||||
],
|
||||
[
|
||||
'DST repeated local time',
|
||||
'25.10.2026',
|
||||
'01:30',
|
||||
$lisbon,
|
||||
test_timestamp('2026-10-25T01:30:00+00:00', $utc),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($validParseCases as [$label, $date, $time, $timezone, $expected]) {
|
||||
$actual = TravianDateTime::getServerStartTimestamp($date, $time, $timezone);
|
||||
if ($actual !== $expected) {
|
||||
fail_server_start_test(
|
||||
$label . ': expected timestamp ' . $expected . ', got ' . var_export($actual, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise every possible H:i value accepted by the configuration format.
|
||||
for ($hour = 0; $hour < 24; $hour++) {
|
||||
for ($minute = 0; $minute < 60; $minute++) {
|
||||
$time = sprintf('%02d:%02d', $hour, $minute);
|
||||
$expected = test_timestamp('2026-06-15 ' . $time . ':00', $utc);
|
||||
$actual = TravianDateTime::getServerStartTimestamp('15.06.2026', $time, $utc);
|
||||
|
||||
if ($actual !== $expected) {
|
||||
fail_server_start_test(
|
||||
'clock sweep ' . $time . ': expected timestamp ' . $expected
|
||||
. ', got ' . var_export($actual, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise every calendar day across a common year and a leap year. Each
|
||||
// parsed timestamp is checked immediately before, at, and after the boundary.
|
||||
$day = new DateTimeImmutable('2023-01-01 12:34:00', $utc);
|
||||
$lastDay = new DateTimeImmutable('2024-12-31 12:34:00', $utc);
|
||||
while ($day <= $lastDay) {
|
||||
$date = $day->format('d.m.Y');
|
||||
$expected = $day->getTimestamp();
|
||||
$actual = TravianDateTime::getServerStartTimestamp($date, '12:34', $utc);
|
||||
|
||||
if ($actual !== $expected) {
|
||||
fail_server_start_test(
|
||||
'calendar sweep ' . $date . ': expected timestamp ' . $expected
|
||||
. ', got ' . var_export($actual, true)
|
||||
);
|
||||
}
|
||||
|
||||
if (TravianDateTime::hasServerStarted($date, '12:34', $expected - 1, $utc)) {
|
||||
fail_server_start_test('calendar sweep ' . $date . ': started one second early');
|
||||
}
|
||||
if (!TravianDateTime::hasServerStarted($date, '12:34', $expected, $utc)) {
|
||||
fail_server_start_test('calendar sweep ' . $date . ': not started at exact boundary');
|
||||
}
|
||||
if (!TravianDateTime::hasServerStarted($date, '12:34', $expected + 1, $utc)) {
|
||||
fail_server_start_test('calendar sweep ' . $date . ': not started after boundary');
|
||||
}
|
||||
|
||||
$day = $day->modify('+1 day');
|
||||
}
|
||||
|
||||
$invalidParseCases = [
|
||||
['blank date', '', '12:00', $utc],
|
||||
['blank time', '20.09.2026', '', $utc],
|
||||
['wrong separator', '20-09-2026', '12:00', $utc],
|
||||
['un-padded date', '20.9.2026', '12:00', $utc],
|
||||
['two-digit year', '20.09.26', '12:00', $utc],
|
||||
['zero day', '00.09.2026', '12:00', $utc],
|
||||
['day too large', '32.09.2026', '12:00', $utc],
|
||||
['zero month', '20.00.2026', '12:00', $utc],
|
||||
['month too large', '20.13.2026', '12:00', $utc],
|
||||
['invalid month length', '31.04.2026', '12:00', $utc],
|
||||
['invalid non-leap day', '29.02.2025', '12:00', $utc],
|
||||
['invalid non-leap century', '29.02.2100', '12:00', $utc],
|
||||
['un-padded time', '20.09.2026', '9:00', $utc],
|
||||
['wrong time separator', '20.09.2026', '09.00', $utc],
|
||||
['hour too large', '20.09.2026', '24:00', $utc],
|
||||
['minute too large', '20.09.2026', '23:60', $utc],
|
||||
['unexpected seconds', '20.09.2026', '12:00:00', $utc],
|
||||
['non-numeric time', '20.09.2026', 'noon', $utc],
|
||||
['NUL byte in date', "20.\0.09.2026", '12:00', $utc],
|
||||
['NUL byte in time', '20.09.2026', "12:\000", $utc],
|
||||
['DST skipped local time', '29.03.2026', '01:30', $lisbon],
|
||||
];
|
||||
|
||||
foreach ($invalidParseCases as [$label, $date, $time, $timezone]) {
|
||||
$actual = TravianDateTime::getServerStartTimestamp($date, $time, $timezone);
|
||||
if ($actual !== null) {
|
||||
fail_server_start_test(
|
||||
$label . ': expected invalid configuration, got timestamp ' . $actual
|
||||
);
|
||||
}
|
||||
|
||||
if (TravianDateTime::hasServerStarted($date, $time, PHP_INT_MAX, $timezone)) {
|
||||
fail_server_start_test($label . ': invalid configuration must fail closed');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that omitting the timezone uses PHP's configured server timezone at
|
||||
// the exact decision boundary, rather than silently falling back to UTC.
|
||||
$originalTimezone = date_default_timezone_get();
|
||||
date_default_timezone_set('Asia/Kathmandu');
|
||||
try {
|
||||
$defaultTimezoneStart = test_timestamp('2026-01-01 00:00:00', $kathmandu);
|
||||
if (TravianDateTime::hasServerStarted('01.01.2026', '00:00', $defaultTimezoneStart - 1)) {
|
||||
fail_server_start_test('default timezone: started one second early');
|
||||
}
|
||||
if (!TravianDateTime::hasServerStarted('01.01.2026', '00:00', $defaultTimezoneStart)) {
|
||||
fail_server_start_test('default timezone: not started at exact boundary');
|
||||
}
|
||||
} finally {
|
||||
date_default_timezone_set($originalTimezone);
|
||||
}
|
||||
|
||||
fwrite(STDOUT, "Server start time regression tests passed.\n");
|
||||
Reference in New Issue
Block a user