diff --git a/backend/modules/gameplay/models/TreasureFinder.php b/backend/modules/gameplay/models/TreasureFinder.php
index 1944a62ba..5c00fde43 100644
--- a/backend/modules/gameplay/models/TreasureFinder.php
+++ b/backend/modules/gameplay/models/TreasureFinder.php
@@ -6,7 +6,7 @@ class TreasureFinder extends \yii\base\Model
public static function findByEncryptedCode($secretKey, $string)
{
return \Yii::$app->db->createCommand("
- SELECT treasure.id as treasure_id, player.id AS player_id
+ SELECT treasure.id as treasure_id, player.id AS player_id, md5(HEX(AES_ENCRYPT(CONCAT(code, player.id), :secretKey))) as encryptedCode
FROM treasure, player
WHERE md5(HEX(AES_ENCRYPT(CONCAT(code, player.id), :secretKey))) LIKE :code
", [
diff --git a/backend/modules/moderation/controllers/AbuserController.php b/backend/modules/moderation/controllers/AbuserController.php
index 71b4b136e..9b0347750 100644
--- a/backend/modules/moderation/controllers/AbuserController.php
+++ b/backend/modules/moderation/controllers/AbuserController.php
@@ -2,6 +2,7 @@
namespace app\modules\moderation\controllers;
+use Yii;
use app\modules\moderation\models\Abuser;
use app\modules\moderation\models\AbuserSearch;
use yii\web\Controller;
diff --git a/backend/modules/moderation/models/Abuser.php b/backend/modules/moderation/models/Abuser.php
index ccdcd9238..4bc76d9d3 100644
--- a/backend/modules/moderation/models/Abuser.php
+++ b/backend/modules/moderation/models/Abuser.php
@@ -131,40 +131,105 @@ public static function find()
*/
public function forFailedClaim()
{
- $string = preg_replace_callback('/(etsctf|tsctf|sctf|ctf|tf|f)_([a-zA-Z0-9_]+)/i', function ($matches) {
- return trim($matches[2]);
- }, $this->title);
- $secretKey = \Yii::$app->sys->treasure_secret_key;
+ if (preg_match('/(?:etsctf|tsctf|sctf|ctf|tf|f)_([a-z0-9]+)/i', $this->title, $matches)) {
+ $claimedHash = $matches[0];
+ $string = $matches[1];
+ } else {
+ $claimedHash = $string = null;
+ }
+ if ($string != null) {
+ $secretKey = \Yii::$app->sys->treasure_secret_key;
+ $result = \app\modules\gameplay\models\TreasureFinder::findByEncryptedCode($secretKey, '%' . $string . '%');
+ if (is_array($result) && $result !== []) {
+ if (array_key_exists('player_id', $result)) {
+ $originalPlayer = \app\modules\frontend\models\Player::findOne($result['player_id']);
+ }
+
+ if (array_key_exists('treasure_id', $result)) {
+ $originalTreasure = \app\modules\gameplay\models\Treasure::findOne($result['treasure_id']);
+ }
+
+ // check if its just a typo for the same user so they havent copied a flag
+ if (array_key_exists('player_id', $result) && intval($result['player_id']) == intval($this->player_id)) {
+ return 'False alarm this is their own code';
+ }
+
+ $profileLink = \app\widgets\ProfileLink::widget([
+ 'username' => $originalPlayer->username,
+ 'actions' => false
+ ]);
+ $offenderLink = \app\widgets\ProfileLink::widget([
+ 'username' => $this->player->username,
+ 'actions' => false
+ ]);
+ if ($this->player->teamPlayer)
+ $from = sprintf("[%s] from team [%s]", $offenderLink, $this->player->teamPlayer->team->name);
+ else
+ $from = "[$offenderLink]";
+
+ if ($originalPlayer->teamPlayer)
+ $to = sprintf("[%s] from team [%s]", $profileLink, $originalPlayer->teamPlayer->team->name);
+ else
+ $to = "[$profileLink]";
+ $msg = "- $from tried to claim code [" . trim($claimedHash) . "] that belongs to player $to for target [" . $originalTreasure->target->name . "] and treasure [" . $originalTreasure->name . "]";
+ return $msg;
+ }
+ }
+
+ return null;
+ }
+ /**
+ * Check if the claimed hash is a near-typo of the player's own actual flag.
+ * Returns a "false alarm" message if it looks like a typo, null otherwise.
+ */
+ private function checkForTypo(string $claimedHash, $actualHash): ?string
+ {
+ $actual = trim($actualHash); // adjust to however you retrieve the player's actual flag
+ $claimed = trim($claimedHash);
- $result = \app\modules\gameplay\models\TreasureFinder::findByEncryptedCode($secretKey, '%' . $string . '%');
+ if ($actual === '') {
+ return null;
+ }
- if (is_array($result) && $result !== []) {
- if (array_key_exists('player_id', $result)) {
- $originalPlayer = \app\modules\frontend\models\Player::findOne($result['player_id']);
- }
- if (array_key_exists('treasure_id', $result)) {
- $originalTreasure = \app\modules\gameplay\models\Treasure::findOne($result['treasure_id']);
+ $distance = levenshtein(strtolower($claimed), strtolower($actual));
+
+ if ($distance === 0 || $distance > 2) {
+ return null;
+ }
+
+ $offenderLink = \app\widgets\ProfileLink::widget([
+ 'username' => $this->player->username,
+ 'actions' => false
+ ]);
+
+ $diff = $this->buildInlineDiff($claimed, $actual);
+
+ return "False alarm, [$offenderLink] had a typo in their claim: $diff";
+ }
+
+ /**
+ * Build a character-level inline diff between two strings.
+ * Mismatched characters are highlighted: red for claimed, green for actual.
+ */
+ private function buildInlineDiff(string $claimed, string $actual): string
+ {
+ $maxLen = max(strlen($claimed), strlen($actual));
+ $claimedOut = '';
+ $actualOut = '';
+
+ for ($i = 0; $i < $maxLen; $i++) {
+ $c = $claimed[$i] ?? '';
+ $a = $actual[$i] ?? '';
+
+ if ($c === $a) {
+ $claimedOut .= htmlspecialchars($c);
+ $actualOut .= htmlspecialchars($a);
+ } else {
+ if ($c !== '') $claimedOut .= '' . htmlspecialchars($c) . '';
+ if ($a !== '') $actualOut .= '' . htmlspecialchars($a) . '';
}
- $profileLink = \app\widgets\ProfileLink::widget([
- 'username' => $originalPlayer->username,
- 'actions' => false
- ]);
- $offenderLink = \app\widgets\ProfileLink::widget([
- 'username' => $this->player->username,
- 'actions' => false
- ]);
- if ($this->player->teamPlayer)
- $from = sprintf("[%s] from team [%s]", $offenderLink, $this->player->teamPlayer->team->name);
- else
- $from = "[$offenderLink]";
-
- if ($originalPlayer->teamPlayer)
- $to = sprintf("[%s] from team [%s]", $profileLink, $originalPlayer->teamPlayer->team->name);
- else
- $to = "[$profileLink]";
- $msg = "- $from tried to claim code [".trim($this->title)."] that belongs to player $to for target [" . $originalTreasure->target->name . "] and treasure [" . $originalTreasure->name . "]";
- return $msg;
}
- return null;
+
+ return "$claimedOut vs $actualOut";
}
}