Fix some atomic exploit

Fix some atomic exploit
This commit is contained in:
novgorodschi catalin
2026-08-13 11:51:54 +03:00
parent 7bad451a83
commit 35f4075734
8 changed files with 708 additions and 62 deletions
@@ -347,6 +347,130 @@ trait AutomationBattleResolution {
return (mysqli_affected_rows($database->dblink) === 1);
}
/**
* Claim atomic pentru un retur de trupe.
*
* Un attack ref poate avea legitim un singur movement de retur
* (sort_type = 4).
*
* Daca exista deja un alt return pentru acelasi ref care a fost
* procesat, acest movement este considerat duplicat si este marcat
* processed fara a mai credita trupele.
*/
private function claimReturnMovementRecord($moveid, $ref) {
global $database;
$moveid = (int)$moveid;
$ref = (int)$ref;
if ($moveid <= 0 || $ref <= 0) {
return false;
}
/*
* Lock pe ATTACK REF, nu pe moveid.
*
* Astfel doua movement-uri duplicate cu moveid diferit
* nu pot trece simultan de verificarea de unicitate.
*/
$lockResult = mysqli_query(
$database->dblink,
"SELECT GET_LOCK('return_attack_ref_$ref', 10) AS locked"
);
if (!$lockResult) {
return false;
}
$lockRow = mysqli_fetch_assoc($lockResult);
if (
!isset($lockRow['locked']) ||
(int)$lockRow['locked'] !== 1
) {
return false;
}
try {
/*
* Verificam daca acest movement mai este pending.
*/
$q = "
SELECT moveid
FROM " . TB_PREFIX . "movement
WHERE moveid = $moveid
AND sort_type = 4
AND proc = 0
LIMIT 1
";
$result = mysqli_query(
$database->dblink,
$q
);
if (!$result || mysqli_num_rows($result) === 0) {
return false;
}
/*
* Verificam daca un alt return pentru acelasi attack
* a fost deja procesat.
*/
$q = "
SELECT moveid
FROM " . TB_PREFIX . "movement
WHERE ref = $ref
AND sort_type = 4
AND proc = 1
AND moveid <> $moveid
LIMIT 1
";
$result = mysqli_query(
$database->dblink,
$q
);
if ($result && mysqli_num_rows($result) > 0) {
/*
* Este un duplicate.
*
* Il consumam fara sa adaugam trupele.
*/
mysqli_query(
$database->dblink,
"UPDATE " . TB_PREFIX . "movement
SET proc = 1
WHERE moveid = $moveid
AND proc = 0"
);
return false;
}
/*
* Claim atomic pentru movement-ul legitim.
*/
$result = mysqli_query(
$database->dblink,
"UPDATE " . TB_PREFIX . "movement
SET proc = 1
WHERE moveid = $moveid
AND proc = 0"
);
return $result && mysqli_affected_rows($database->dblink) === 1;
} finally {
mysqli_query(
$database->dblink,
"SELECT RELEASE_LOCK('return_attack_ref_$ref')"
);
}
}
/**
* Handle hero evasion: if the defender has evasion active and can afford it,
@@ -211,7 +211,7 @@ trait AutomationTroopMovements {
$time = time();
$q = "
SELECT
`to`, `from`, moveid, starttime, endtime, wood, clay, iron, crop,
`to`, `from`, moveid, ref, starttime, endtime, wood, clay, iron, crop,
t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11
FROM
".TB_PREFIX."movement,
@@ -238,9 +238,11 @@ trait AutomationTroopMovements {
$database->getOasisEnforce($vilIDs, 1);
foreach($dataarray as $data) {
if (!$this->claimMovementRecord($data['moveid'])) {
continue;
}
if (!$this->claimReturnMovementRecord(
$data['moveid'],
$data['ref'])) {
continue;
}
$tribe = $database->getUserField($database->getVillageField($data['to'], "owner"), "tribe", 0);
$u = $tribe == 1 ? "" : $tribe - 1;
@@ -192,6 +192,52 @@ trait DatabaseMovementQueries {
return (mysqli_affected_rows($this->dblink) === 1);
}
/**
* Revendicare ATOMICA a unei miscari care urmeaza sa fie anulata din rally point
* (build.php?mode=troops&cancel=1&moveid=X).
*
* DE CE EXISTA: varianta veche din build.php facea, in trei pasi separati:
* SELECT COUNT(*) ... WHERE proc = 0 AND moveid = X -> daca Total == 1
* UPDATE ... SET proc = 1 WHERE proc = 0 AND moveid = X
* addMovement(4, to, from, REF-UL VECHI, now, end)
* Doua request-uri simultane treceau AMANDOUA de SELECT COUNT (ambele vad Total = 1),
* UPDATE-ul era idempotent, deci amandoua ajungeau la addMovement() si inserau DOUA
* randuri sort_type = 4 cu ACELASI ref. Cron-ul (AutomationTroopMovements) livreaza
* trupele o data PER RAND DE MOVEMENT, nu per ref => trupele se dublau la fiecare
* ciclu trimite-anuleaza: 30k -> 60k -> 120k -> 242k -> ...
*
* Acum proprietatea, tipul, fereastra de 90 de secunde si proc = 0 sunt toate in
* acelasi UPDATE conditionat. Doar request-ul care chiar modifica randul primeste
* true (affected_rows === 1); toate celelalte pierd cursa si nu creeaza nimic.
*
* ANOMALIE SEMNALATA, NU SCHIMBATA TACIT: codul vechi nu verifica deloc sort_type,
* deci se putea "anula" si un sort_type = 4 (retur) al carui `from` era satul tau -
* adica returul unei intariri straine care pleaca din satul tau - iar returul nou
* generat aducea acele trupe LA TINE. Restrictia sort_type IN (3, 5) inchide si
* acest vector si pastreaza exact cele doua cazuri legitime din 16_walking.tpl:
* atac/intarire iesita din sat (3) si colonisti (5).
*
* @param int $moveid ID-ul miscarii
* @param int $wid Satul curent (trebuie sa fie expeditorul)
* @return bool True doar pentru request-ul care a revendicat miscarea
*/
function claimMovementCancel($moveid, $wid) {
$moveid = (int) $moveid;
$wid = (int) $wid;
if ($moveid <= 0 || $wid <= 0) return false;
$limit = time() - 90;
$q = "UPDATE " . TB_PREFIX . "movement SET proc = 1 WHERE moveid = $moveid AND proc = 0 AND `from` = $wid AND sort_type IN (3, 5) AND starttime > $limit";
if (!mysqli_query($this->dblink, $q)) {
return false;
}
return (mysqli_affected_rows($this->dblink) === 1);
}
// no need to cache this method
function getA2b($ckey) {
list($ckey) = $this->escape_input($ckey);
@@ -740,4 +786,4 @@ trait DatabaseMovementQueries {
self::$prisonersCache = [];
}
}
}
@@ -95,6 +95,79 @@ trait DatabaseTroopQueries {
$id = (int) $id;
mysqli_query($this->dblink, "SELECT RELEASE_LOCK('enforce_$id')");
}
/**
* Lock pentru procesarea returului trupelor dintr-o oaza.
*
* Este separat de enforce_$id deoarece o oaza poate avea mai multe
* randuri de reinforcement.
*
* Scop:
* Request A -> citeste reinforcement din oasis
* Request B -> citeste acelasi reinforcement
* => fara lock, ambele pot crea movement de retur.
*/
function getOasisReturnLock($wref) {
$wref = (int) $wref;
$result = mysqli_query(
$this->dblink,
"SELECT GET_LOCK('oasis_return_$wref', 10) AS locked"
);
if (!$result) {
return false;
}
$row = mysqli_fetch_assoc($result);
return isset($row['locked']) && (int)$row['locked'] === 1;
}
/**
* Elibereaza lock-ul pentru returul unei oaze.
*/
function releaseOasisReturnLock($wref) {
$wref = (int) $wref;
mysqli_query(
$this->dblink,
"SELECT RELEASE_LOCK('oasis_return_$wref')"
);
}
/**
* Citeste DIRECT din DB toate reinforcement-urile aflate in oaza.
*
* IMPORTANT:
* Nu folosim cache aici. Functia este apelata DUPA obtinerea lock-ului
* pentru a preveni TOCTOU/race condition.
*/
function getOasisEnforceByWref($wref, $use_cache = false) {
$wref = (int) $wref;
if ($wref <= 0) {
return [];
}
$q = "
SELECT e.*, o.conqured
FROM " . TB_PREFIX . "enforcement AS e
LEFT JOIN " . TB_PREFIX . "odata AS o
ON e.vref = o.wref
WHERE e.vref = $wref
AND o.wref = $wref
AND o.conqured > 0
";
$result = mysqli_query($this->dblink, $q);
if (!$result) {
return [];
}
return $this->mysqli_fetch_all($result);
}
/**
* Add the unit table(s) and troops if presents
+371 -38
View File
@@ -351,35 +351,353 @@ class Units {
return "";
}
/**
* Returneaza trupele stationate in sat si/sau in oazele acestuia.
*
* IMPORTANT:
* - fiecare reinforcement este procesat sub lock;
* - fiecare oaza este procesata sub un lock separat;
* - datele sunt re-citite din DB dupa obtinerea lock-ului;
* - nu folosim cache pentru datele critice.
*
* @param int $wref
* @param int $mode
*
* mode = 0:
* returneaza reinforcement-urile din sat + oazele lui
*
* mode = 1:
* returneaza doar reinforcement-urile din oazele satului
*/
public function returnTroops($wref, $mode = 0) {
global $database;
if(!$mode){
$getenforce = $database->getEnforceVillage($wref, 0);
foreach($getenforce as $enforce) $this->processReturnTroops($enforce);
}
// check oasis
$getenforce1 = $database->getOasisEnforce($wref, 1);
foreach($getenforce1 as $enforce) $this->processReturnTroops($enforce);
// set oasis to default
if(count($getenforce1) > 0) $database->regenerateOasisUnits($getenforce1[0]['vref']);
$wref = (int) $wref;
if ($wref <= 0) {
return;
}
/*
* Reinforcement-uri stationate direct in sat.
*
* Le procesam individual sub enforce lock.
*/
if (!$mode) {
$getenforce = $database->getEnforceVillage($wref, 0, false);
if ($getenforce && count($getenforce)) {
foreach ($getenforce as $enforce) {
if (!empty($enforce['id'])) {
$this->returnEnforcementRecord((int)$enforce['id']);
}
}
}
}
/*
* OAZE
*
* getOasisEnforce() ne da toate reinforcement-urile din oazele
* cucerite de acest sat.
*
* Nu procesam direct rezultatul deoarece acesta poate fi cached.
* Extragem doar ID-urile oazelor si apoi fiecare oaza este re-citita
* dupa obtinerea lock-ului.
*/
$getenforce1 = $database->getOasisEnforce($wref, 1, false);
if (!$getenforce1 || !count($getenforce1)) {
return;
}
$oasisRefs = [];
foreach ($getenforce1 as $enforce) {
$oasisWref = isset($enforce['vref']) ? (int)$enforce['vref'] : 0;
if ($oasisWref > 0) {
$oasisRefs[$oasisWref] = true;
}
}
foreach (array_keys($oasisRefs) as $oasisWref) {
$this->returnOasisTroops($oasisWref);
}
}
/**
* Returneaza TOATE reinforcement-urile dintr-o singura oaza.
*
* Aceasta este protectia principala impotriva exploitului de duplicare.
*
* Doua request-uri simultane pentru aceeasi oaza:
*
* Request A -> obtine lock
* Request B -> asteapta
*
* A -> citeste reinforcement
* A -> creeaza movement
* A -> sterge reinforcement
* A -> release lock
*
* B -> obtine lock
* B -> re-citeste DB
* B -> nu mai gaseste reinforcement
* B -> nu mai poate duplica nimic
*/
public function returnOasisTroops($oasisWref) {
global $database;
$oasisWref = (int)$oasisWref;
if ($oasisWref <= 0) {
return false;
}
if (!$database->getOasisReturnLock($oasisWref)) {
return false;
}
try {
/*
* IMPORTANT:
* Re-fetch DIRECT din DB dupa lock.
* Nu folosim getOasisEnforce(..., cache).
*/
$reinforcements = $database->getOasisEnforceByWref(
$oasisWref,
false
);
if (!$reinforcements || !count($reinforcements)) {
return false;
}
foreach ($reinforcements as $enforce) {
if (empty($enforce['id'])) {
continue;
}
/*
* processReturnTroops() sterge reinforcement-ul dupa ce
* creeaza movement-ul de retur.
*/
$this->processReturnTroops($enforce);
}
/*
* Comportamentul original:
* dupa ce reinforcement-urile au fost returnate, oaza isi
* regenereaza trupele naturale.
*/
$database->regenerateOasisUnits($oasisWref);
return true;
} finally {
$database->releaseOasisReturnLock($oasisWref);
}
}
/**
* Returneaza un singur reinforcement record.
*
* Protectie suplimentara pentru cazurile in care un sat este sters
* sau reinforcement-ul este procesat dintr-o alta cale.
*/
private function returnEnforcementRecord($enforceId) {
global $database;
$enforceId = (int)$enforceId;
if ($enforceId <= 0) {
return false;
}
if (!$database->getEnforceLock($enforceId)) {
return false;
}
try {
/*
* Re-fetch dupa lock.
*/
$enforce = $database->getEnforceArray(
$enforceId,
0,
false
);
/*
* Poate sa fi fost deja procesat de alt request.
*/
if (!$enforce || empty($enforce['id'])) {
return false;
}
$this->processReturnTroops($enforce);
return true;
} finally {
$database->releaseEnforceLock($enforceId);
}
}
/**
* Creeaza movement-ul de retur pentru un reinforcement.
*
* ATENTIE:
* Aceasta functie NU mai este responsabila de locking.
* Lock-ul este facut de:
*
* returnEnforcementRecord()
* returnOasisTroops()
*
* astfel incat sa avem o singura responsabilitate pentru lock.
*/
private function processReturnTroops($enforce) {
global $database;
$to = $database->getVillage($enforce['from']);
$tribe = $database->getUserField($to['owner'], 'tribe', 0);
if (empty($enforce['id']) || empty($enforce['from'])) {
return false;
}
$fromWref = (int)$enforce['from'];
$oasisWref = (int)$enforce['vref'];
/*
* Satul de origine al reinforcement-ului.
*/
$to = $database->getVillage($fromWref);
if (!$to || empty($to['owner'])) {
return false;
}
$tribe = (int)$database->getUserField(
$to['owner'],
'tribe',
0
);
if ($tribe <= 0) {
return false;
}
$start = ($tribe - 1) * 10 + 1;
$troopsTime = $this->getWalkingTroopsTime($enforce['from'], $enforce['vref'], $to['owner'], $tribe, $enforce, 1);
$time = $database->getArtifactsValueInfluence($from['owner'], $enforce['from'], 2, $troopsTime);
$reference = $database->addAttack($enforce['from'], $enforce['u'.$start], $enforce['u'.($start + 1)], $enforce['u'.($start + 2)], $enforce['u'.($start + 3)], $enforce['u'.($start + 4)], $enforce['u'.($start + 5)], $enforce['u'.($start + 6)], $enforce['u'.($start + 7)], $enforce['u'.($start + 8)], $enforce['u'.($start + 9)], $enforce['hero'], 2, 0, 0, 0, 0);
$database->addMovement(4, $enforce['vref'], $enforce['from'], $reference, time(), ($time + time()));
$database->deleteReinf($enforce['id']);
/*
* Calculeaza timpul de mers Oaza -> Sat.
*/
$troopsTime = $this->getWalkingTroopsTime(
$fromWref,
$oasisWref,
$to['owner'],
$tribe,
$enforce,
1
);
/*
* BUG FIX:
*
* Codul vechi folosea:
*
* $from['owner']
*
* dar $from nu exista in aceasta functie.
*
* Proprietarul corect este $to['owner'].
*/
$time = $database->getArtifactsValueInfluence(
$to['owner'],
$fromWref,
2,
$troopsTime
);
/*
* Unit-urile din reinforcement sunt stocate in coloanele
* corespunzatoare tribului proprietarului.
*/
$t1 = (int)($enforce['u' . $start] ?? 0);
$t2 = (int)($enforce['u' . ($start + 1)] ?? 0);
$t3 = (int)($enforce['u' . ($start + 2)] ?? 0);
$t4 = (int)($enforce['u' . ($start + 3)] ?? 0);
$t5 = (int)($enforce['u' . ($start + 4)] ?? 0);
$t6 = (int)($enforce['u' . ($start + 5)] ?? 0);
$t7 = (int)($enforce['u' . ($start + 6)] ?? 0);
$t8 = (int)($enforce['u' . ($start + 7)] ?? 0);
$t9 = (int)($enforce['u' . ($start + 8)] ?? 0);
$t10 = (int)($enforce['u' . ($start + 9)] ?? 0);
$hero = (int)($enforce['hero'] ?? 0);
/*
* Nu cream movement pentru un reinforcement gol.
*/
if (
$t1 <= 0 &&
$t2 <= 0 &&
$t3 <= 0 &&
$t4 <= 0 &&
$t5 <= 0 &&
$t6 <= 0 &&
$t7 <= 0 &&
$t8 <= 0 &&
$t9 <= 0 &&
$t10 <= 0 &&
$hero <= 0
) {
$database->deleteReinf((int)$enforce['id']);
return false;
}
/*
* Creeaza attack record pentru retur.
*/
$reference = $database->addAttack(
$fromWref,
$t1,
$t2,
$t3,
$t4,
$t5,
$t6,
$t7,
$t8,
$t9,
$t10,
$hero,
2,
0,
0,
0,
0
);
/*
* Creeaza EXACT UN singur movement de retur.
*/
$now = time();
$database->addMovement(
4,
$oasisWref,
$fromWref,
$reference,
$now,
$now + $time
);
/*
* Acum reinforcement-ul nu mai poate fi procesat din nou,
* deoarece row-ul este sters IN TIMP CE lock-ul este detinut.
*/
$database->deleteReinf((int)$enforce['id']);
return true;
}
private function sendTroops($post) {
@@ -639,23 +957,38 @@ class Units {
$to = $database->getVillage( $enforce['from'] );
$Gtribe = ($ownerTribe = $database->getUserField( $to['owner'], 'tribe', 0)) == 1 ? "" : $ownerTribe - 1;
for ( $i = 1; $i < 10; $i ++ ) {
if ( isset( $post[ 't' . $i ] ) ) {
if ( $i != 10 ) {
if ( $post[ 't' . $i ] > $enforce[ 'u' . $Gtribe . $i ] ) {
$form->addError( "error", "You can't send back more units than you have" );
break;
}
for ($i = 1; $i <= 10; $i++) {
if (!isset($post['t'.$i])) {
$post['t'.$i] = 0;
continue;
}
if ( $post[ 't' . $i ] < 0 ) {
$form->addError( "error", "You can't send back negative units." );
break;
}
}
} else {
$post[ 't' . $i . '' ] = '0';
}
}
if (!is_numeric($post['t'.$i])) {
$form->addError(
"error",
"Invalid troop amount."
);
break;
}
$post['t'.$i] = (int)$post['t'.$i];
if ($post['t'.$i] < 0) {
$form->addError(
"error",
"You can't send back negative units."
);
break;
}
if ($post['t'.$i] > (int)$enforce['u'.$Gtribe.$i]) {
$form->addError(
"error",
"You can't send back more units than you have"
);
break;
}
}
if ( isset( $post['t11'] ) ) {
if ( $post['t11'] > $enforce['hero'] ) {
$form->addError( "error", "You can't send back more units than you have" );
+53 -5
View File
@@ -36,11 +36,59 @@
}
$wwMultiplier = $wwFactor / 0.25; // 1.0 fara Waterworks, 1.5 la nivel maxim
if (isset($_GET['gid']) && $_GET['gid'] == 37 && isset($_GET['del']) && $database->getOasisField($_GET['del'], 'owner') == $session->uid) {
$units->returnTroops($village->wid, 1);
$database->removeOases($_GET['del']);
header("Location: build.php?id=" . $id . "&land");
exit;
if (
isset($_GET['gid']) &&
(int)$_GET['gid'] === 37 &&
isset($_GET['del'])
) {
$oasisWref = (int)$_GET['del'];
/*
* Verificam server-side ca:
*
* 1. oaza exista;
* 2. apartine jucatorului;
* 3. este cucerita de SATUL CURENT.
*
* Nu este suficient doar owner == session uid deoarece un jucator
* poate avea mai multe sate si mai multe oaze.
*/
$oasisOwner = (int)$database->getOasisField(
$oasisWref,
'owner'
);
$oasisConquered = (int)$database->getOasisField(
$oasisWref,
'conqured'
);
if (
$oasisWref > 0 &&
$oasisOwner === (int)$session->uid &&
$oasisConquered === (int)$village->wid
) {
/*
* FOARTE IMPORTANT:
*
* Returnam doar trupele din OAZA SELECTATA.
*
* returnTroops($village->wid, 1) NU trebuie folosit aici,
* deoarece acela proceseaza toate oazele satului.
*
* Functia are lock per oasis si re-citeste DB dupa lock.
*/
$units->returnOasisTroops($oasisWref);
/*
* Dupa ce reinforcement-urile au fost returnate,
* oaza este eliberata.
*/
$database->removeOases($oasisWref);
}
header("Location: build.php?id=" . $id . "&land");
exit;
}
// Explicit lookup, instead of the original repetitive switch:
+1 -1
View File
@@ -137,7 +137,7 @@ if ($displayarray['vac_mode'] == 1)
<?php
if ($displayarray['access'] == BANNED) {
echo "<tr><td colspan='2'><center><b>".BANNED."</b></center></td></tr>";
echo "<tr><td colspan='2'><center><b>BANNED</b></center></td></tr>";
}
?>
+33 -13
View File
@@ -273,22 +273,42 @@ else $create = 0;
if(isset($_POST['a']) == 533374 && isset($_POST['id']) == 39) $units->Settlers($_POST);
/**
* Anularea unei miscari din rally point (linkul "x" din 16_walking.tpl).
*
* EXPLOIT REPARAT (duplicare de trupe): varianta veche verifica intai
* "SELECT Count(*) ... where proc = 0 and moveid = X", apoi facea UPDATE proc = 1
* si abia apoi addMovement(4, ..., ref-ul miscarii anulate). Intre COUNT si UPDATE
* exista o fereastra TOCTOU: doua request-uri trimise simultan treceau amandoua de
* COUNT si inserau DOUA movement-uri de retur cu ACELASI ref. Cron-ul livreaza
* trupele o data pentru fiecare rand de movement, deci un ciclu trimite-anuleaza
* dubla trupele. Repetat de cateva ori: 30k -> 60k -> 120k -> 242k -> ... -> milioane.
*
* Acum revendicarea se face intr-un singur UPDATE conditionat, in
* $database->claimMovementCancel(), care verifica atomic: proc = 0, expeditorul e
* satul curent, sort_type IN (3, 5) si fereastra de 90 de secunde. Returul se
* creeaza doar daca ACEST request a fost cel care a schimbat randul.
*
* Doua lucruri semnalate, nu schimbate tacit:
* - $q2 ("SELECT id FROM send ORDER BY id DESC") si $lastid erau cod mort:
* rezultatul nu era folosit nicaieri. Eliminat.
* - $_GET['id'] intra brut in antetul Location. Trecut acum prin (int),
* ca in restul fisierului.
*/
if(isset($_GET['mode']) && $_GET['mode'] == 'troops' && isset($_GET['cancel']) && $_GET['cancel'] == 1){
$oldmovement = $database->getMovementById($_GET['moveid']);
$now = time();
if(($now - $oldmovement[0]['starttime']) < 90 && $oldmovement[0]['from'] == $village->wid){
$qc = "SELECT Count(*) as Total FROM " . TB_PREFIX . "movement where proc = 0 and moveid = " . $database->escape((int)$_GET['moveid']);
$resultc = mysqli_fetch_array(mysqli_query($database->dblink, $qc), MYSQLI_ASSOC);
if($resultc['Total'] == 1){
$q = "UPDATE " . TB_PREFIX . "movement set proc = 1 where proc = 0 and moveid = " . $database->escape((int)$_GET['moveid']);
$database->query($q);
$end = $now + ($now - $oldmovement[0]['starttime']);
$q2 = "SELECT id FROM " . TB_PREFIX . "send ORDER BY id DESC";
$lastid = mysqli_fetch_array(mysqli_query($database->dblink, $q2));
$database->addMovement(4, $oldmovement[0]['to'], $oldmovement[0]['from'], $oldmovement[0]['ref'], $now, $end);
$moveid = isset($_GET['moveid']) && is_numeric($_GET['moveid']) ? (int) $_GET['moveid'] : 0;
if($moveid > 0 && $database->claimMovementCancel($moveid, $village->wid)){
$oldmovement = $database->getMovementById($moveid);
if(!empty($oldmovement)){
$now = time();
$end = $now + ($now - (int) $oldmovement[0]['starttime']);
$database->addMovement(4, (int) $oldmovement[0]['to'], (int) $oldmovement[0]['from'], (int) $oldmovement[0]['ref'], $now, $end);
}
}
header("Location: " . $_SERVER['PHP_SELF'] . "?id=" . $_GET['id']);
header("Location: " . $_SERVER['PHP_SELF'] . "?id=" . (isset($_GET['id']) ? (int) $_GET['id'] : 39));
exit();
}