Skip to content

Commit cf86a18

Browse files
Use external ID in UI everywhere instead of internal database ID
Fixes #3024
1 parent da3e352 commit cf86a18

File tree

182 files changed

+2026
-1329
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

182 files changed

+2026
-1329
lines changed

ChangeLog

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Version 10.0.0DEV
44
---------------------------
55

66
- Allow teams to be in more than one category and add more category types.
7+
- Do not expose internal IDs in interface or API anymore.
78

89
Version 9.0.0 - 5 October 2025
910
---------------------------
@@ -348,7 +349,7 @@ Version 7.1.0 - 25 September 2019
348349
- Upgrade Symfony and dependencies to version 4.3.
349350
- Use Doctrine for migrations and initial data.
350351
- Add option (enabled by default) to limit files passed to
351-
language compilers by langauge extension list.
352+
language compilers by language extension list.
352353
- Add option to add a whole team category to a contest.
353354
- Allow admin to specify print command using configuration setting.
354355
- Show metadata for judging runs.

judge/judgedaemon.main.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
class JudgeDaemon
2222
{
23+
private const EXTERNAL_IDENTIFIER_REGEX = '/^[a-zA-Z0-9_.-]+$/';
24+
2325
private static ?JudgeDaemon $instance = null;
2426

2527
private ?array $endpoint = null;
@@ -620,7 +622,7 @@ private function checkDiskSpace(string $workdirpath): void
620622

621623
private function judgingDirectory(string $workdirpath, array $judgeTask): string
622624
{
623-
if (filter_var($judgeTask['submitid'], FILTER_VALIDATE_INT) === false ||
625+
if (filter_var($judgeTask['submitid'], FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => static::EXTERNAL_IDENTIFIER_REGEX]]) === false ||
624626
filter_var($judgeTask['jobid'], FILTER_VALIDATE_INT) === false) {
625627
error("Malformed data returned in judgeTask IDs: " . var_export($judgeTask, true));
626628
}
@@ -1284,7 +1286,7 @@ private function compile(
12841286

12851287
// Revoke readablity for domjudge-run user to this workdir.
12861288
chmod($workdir, 0700);
1287-
logmsg(LOG_NOTICE, "Judging s$judgeTask[submitid], task $judgeTask[judgetaskid]: compile error");
1289+
logmsg(LOG_NOTICE, "Submission $judgeTask[submitid], task $judgeTask[judgetaskid]: compile error");
12881290
return false;
12891291
}
12901292

@@ -1348,7 +1350,7 @@ private function compile(
13481350
// What does the exitcode mean?
13491351
if (!isset($this->EXITCODES[$retval])) {
13501352
alert('error');
1351-
$description = "Unknown exitcode from compile.sh for s$judgeTask[submitid]: $retval";
1353+
$description = "Unknown exitcode from compile.sh for $judgeTask[submitid]: $retval";
13521354
logmsg(LOG_ERR, $description);
13531355
$this->disable('compile_script', 'compile_script_id', $judgeTask['compile_script_id'], $description, $judgeTask['judgetaskid'], $compile_output);
13541356

@@ -1565,7 +1567,7 @@ private function runTestcase(
15651567
// What does the exitcode mean?
15661568
if (!isset($this->EXITCODES[$retval])) {
15671569
alert('error');
1568-
error("Unknown exitcode ($retval) from testcase_run.sh for s$judgeTask[submitid]");
1570+
error("Unknown exitcode ($retval) from testcase_run.sh for submission $judgeTask[submitid]");
15691571
}
15701572
$result = $this->EXITCODES[$retval];
15711573

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20251024122021 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Change language ID to be an auto increment integer';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// We need to do some juggling to get this to work:
23+
24+
// - First we drop the foreign keys from tables referencing langid. We also drop compound
25+
$this->addSql('ALTER TABLE submission DROP FOREIGN KEY submission_ibfk_4');
26+
$this->addSql('ALTER TABLE problemlanguage DROP FOREIGN KEY FK_46B150BB2271845');
27+
$this->addSql('ALTER TABLE version DROP FOREIGN KEY FK_BF1CD3C32271845');
28+
$this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43232271845');
29+
30+
// - Then we add a temporary integer langid column to all these tables, since we can't
31+
// change the langid column itself, because MySQL will try to convert the strings to
32+
// integers and fail. We set the langid column in the language table to auto increment,
33+
// so it's filled by MySQL.
34+
$this->addSql('ALTER TABLE language ADD langid_int INT UNSIGNED AUTO_INCREMENT UNIQUE AFTER langid');
35+
$this->addSql('ALTER TABLE contestlanguage ADD langid_int INT UNSIGNED NOT NULL AFTER langid');
36+
$this->addSql('ALTER TABLE problemlanguage ADD langid_int INT UNSIGNED NOT NULL AFTER langid');
37+
$this->addSql('ALTER TABLE submission ADD langid_int INT UNSIGNED DEFAULT NULL AFTER langid');
38+
$this->addSql('ALTER TABLE version ADD langid_int INT UNSIGNED DEFAULT NULL AFTER langid');
39+
40+
// - Now we copy the langid_int values to other tables.
41+
$this->addSql('UPDATE contestlanguage c JOIN language l ON c.langid = l.langid SET c.langid_int = l.langid_int');
42+
$this->addSql('UPDATE problemlanguage p JOIN language l ON p.langid = l.langid SET p.langid_int = l.langid_int');
43+
$this->addSql('UPDATE submission s JOIN language l ON s.langid = l.langid SET s.langid_int = l.langid_int');
44+
$this->addSql('UPDATE version v JOIN language l ON v.langid = l.langid SET v.langid_int = l.langid_int');
45+
$this->addSql('UPDATE auditlog a JOIN language l ON a.dataid = l.langid AND a.datatype = "language" SET a.dataid = l.langid_int WHERE a.dataid IS NOT NULL');
46+
47+
// - Then we drop the old langid columns and drop any (compound) primary keys that use it.
48+
$this->addSql('ALTER TABLE language DROP PRIMARY KEY');
49+
$this->addSql('ALTER TABLE language DROP COLUMN langid');
50+
$this->addSql('ALTER TABLE contestlanguage DROP PRIMARY KEY');
51+
$this->addSql('ALTER TABLE contestlanguage DROP COLUMN langid');
52+
$this->addSql('ALTER TABLE problemlanguage DROP PRIMARY KEY');
53+
$this->addSql('ALTER TABLE problemlanguage DROP COLUMN langid');
54+
$this->addSql('ALTER TABLE submission DROP COLUMN langid');
55+
$this->addSql('ALTER TABLE version DROP COLUMN langid');
56+
57+
// - Next, we rename the langid_int columns back to langid.
58+
$this->addSql('ALTER TABLE language CHANGE langid_int langid INT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT \'Language ID\'');
59+
$this->addSql('ALTER TABLE contestlanguage CHANGE langid_int langid INT UNSIGNED NOT NULL COMMENT \'Language ID\'');
60+
$this->addSql('ALTER TABLE problemlanguage CHANGE langid_int langid INT UNSIGNED NOT NULL COMMENT \'Language ID\'');
61+
$this->addSql('ALTER TABLE submission CHANGE langid_int langid INT UNSIGNED DEFAULT NULL COMMENT \'Language ID\'');
62+
$this->addSql('ALTER TABLE version CHANGE langid_int langid INT UNSIGNED DEFAULT NULL COMMENT \'Language ID\'');
63+
64+
// - Finally we add back all primary and foreign keys.
65+
$this->addSql('ALTER TABLE language ADD PRIMARY KEY (langid), DROP INDEX langid_int');
66+
$this->addSql('ALTER TABLE contestlanguage ADD PRIMARY KEY (cid, langid)');
67+
$this->addSql('ALTER TABLE problemlanguage ADD PRIMARY KEY (probid, langid)');
68+
69+
$this->addSql('ALTER TABLE submission ADD INDEX langid (langid), ADD CONSTRAINT submission_ibfk_4 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
70+
$this->addSql('ALTER TABLE problemlanguage ADD INDEX IDX_46B150BB2271845 (langid), ADD CONSTRAINT FK_46B150BB2271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
71+
$this->addSql('ALTER TABLE version ADD INDEX IDX_BF1CD3C32271845 (langid), ADD CONSTRAINT FK_BF1CD3C32271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
72+
$this->addSql('ALTER TABLE contestlanguage ADD INDEX IDX_ADCB43232271845 (langid), ADD CONSTRAINT FK_ADCB43232271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
73+
}
74+
75+
public function down(Schema $schema): void
76+
{
77+
// When migrating back, we do the logic in reverse but we have custom logic for setting
78+
// the string langid. We set it to the external ID except for a set of 10 known languages
79+
// where the external ID differs from our previously used langid.
80+
81+
$this->addSql('ALTER TABLE submission DROP FOREIGN KEY submission_ibfk_4');
82+
$this->addSql('ALTER TABLE problemlanguage DROP FOREIGN KEY FK_46B150BB2271845');
83+
$this->addSql('ALTER TABLE version DROP FOREIGN KEY FK_BF1CD3C32271845');
84+
$this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43232271845');
85+
86+
$this->addSql('ALTER TABLE language ADD langid_str VARCHAR(32) NOT NULL AFTER langid');
87+
$this->addSql('ALTER TABLE contestlanguage ADD langid_str VARCHAR(32) NOT NULL AFTER langid');
88+
$this->addSql('ALTER TABLE problemlanguage ADD langid_str VARCHAR(32) NOT NULL AFTER langid');
89+
$this->addSql('ALTER TABLE submission ADD langid_str VARCHAR(32) DEFAULT NULL AFTER langid');
90+
$this->addSql('ALTER TABLE version ADD langid_str VARCHAR(32) DEFAULT NULL AFTER langid');
91+
92+
$this->addSql("UPDATE language SET langid_str = CASE externalid
93+
WHEN 'ada' THEN 'adb'
94+
WHEN 'haskell' THEN 'hs'
95+
WHEN 'javascript' THEN 'js'
96+
WHEN 'kotlin' THEN 'kt'
97+
WHEN 'pascal' THEN 'pas'
98+
WHEN 'prolog' THEN 'plg'
99+
WHEN 'python3' THEN 'py3'
100+
WHEN 'python2' THEN 'py2'
101+
WHEN 'ruby' THEN 'rb'
102+
WHEN 'rust' THEN 'rs'
103+
ELSE externalid
104+
END");
105+
106+
$this->addSql('UPDATE contestlanguage c JOIN language l ON c.langid = l.langid SET c.langid_str = l.langid_str');
107+
$this->addSql('UPDATE problemlanguage p JOIN language l ON p.langid = l.langid SET p.langid_str = l.langid_str');
108+
$this->addSql('UPDATE submission s JOIN language l ON s.langid = l.langid SET s.langid_str = l.langid_str');
109+
$this->addSql('UPDATE version v JOIN language l ON v.langid = l.langid SET v.langid_str = l.langid_str');
110+
$this->addSql('UPDATE auditlog a JOIN language l ON a.dataid = l.langid AND a.datatype = "language" SET a.dataid = l.langid_str WHERE a.dataid IS NOT NULL');
111+
112+
$this->addSql('ALTER TABLE language DROP PRIMARY KEY');
113+
$this->addSql('ALTER TABLE language DROP COLUMN langid');
114+
$this->addSql('ALTER TABLE contestlanguage DROP PRIMARY KEY');
115+
$this->addSql('ALTER TABLE contestlanguage DROP COLUMN langid');
116+
$this->addSql('ALTER TABLE problemlanguage DROP PRIMARY KEY');
117+
$this->addSql('ALTER TABLE problemlanguage DROP COLUMN langid');
118+
$this->addSql('ALTER TABLE submission DROP COLUMN langid');
119+
$this->addSql('ALTER TABLE version DROP COLUMN langid');
120+
121+
$this->addSql('ALTER TABLE language CHANGE langid_str langid VARCHAR(32) NOT NULL COMMENT \'Language ID\'');
122+
$this->addSql('ALTER TABLE contestlanguage CHANGE langid_str langid VARCHAR(32) NOT NULL COMMENT \'Language ID\'');
123+
$this->addSql('ALTER TABLE problemlanguage CHANGE langid_str langid VARCHAR(32) NOT NULL COMMENT \'Language ID\'');
124+
$this->addSql('ALTER TABLE submission CHANGE langid_str langid VARCHAR(32) DEFAULT NULL COMMENT \'Language ID\'');
125+
$this->addSql('ALTER TABLE version CHANGE langid_str langid VARCHAR(32) DEFAULT NULL COMMENT \'Language ID\'');
126+
127+
$this->addSql('ALTER TABLE language ADD PRIMARY KEY (langid)');
128+
$this->addSql('ALTER TABLE contestlanguage ADD PRIMARY KEY (cid, langid)');
129+
$this->addSql('ALTER TABLE problemlanguage ADD PRIMARY KEY (probid, langid)');
130+
131+
$this->addSql('ALTER TABLE submission ADD INDEX langid (langid), ADD CONSTRAINT submission_ibfk_4 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
132+
$this->addSql('ALTER TABLE problemlanguage ADD INDEX IDX_46B150BB2271845 (langid), ADD CONSTRAINT FK_46B150BB2271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
133+
$this->addSql('ALTER TABLE version ADD INDEX IDX_BF1CD3C32271845 (langid), ADD CONSTRAINT FK_BF1CD3C32271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
134+
$this->addSql('ALTER TABLE contestlanguage ADD INDEX IDX_ADCB43232271845 (langid), ADD CONSTRAINT FK_ADCB43232271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
135+
}
136+
137+
public function isTransactional(): bool
138+
{
139+
return false;
140+
}
141+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
use phpDocumentor\Reflection\Types\Static_;
10+
11+
final class Version20251026092516 extends AbstractMigration
12+
{
13+
protected const ENTITIES = [
14+
'clarification' => 'clarid',
15+
'contest' => 'cid',
16+
'language' => 'langid',
17+
'problem' => 'probid',
18+
'submission' => 'submitid',
19+
'team' => 'teamid',
20+
'team_affiliation' => 'affilid',
21+
'team_category' => 'categoryid',
22+
'user' => ['userid', 'username'],
23+
];
24+
25+
public function getDescription(): string
26+
{
27+
return 'Make auditlog IDs reference external IDs';
28+
}
29+
30+
public function up(Schema $schema): void
31+
{
32+
// Note: this migration only works for entities that still exist. For others, it will not work but there is nothing we can do
33+
$this->addSql('ALTER TABLE auditlog CHANGE cid cid VARCHAR(255) DEFAULT NULL COMMENT \'External contest ID associated to this entry\', CHANGE dataid dataid VARCHAR(64) DEFAULT NULL COMMENT \'(External) identifier in reference table\'');
34+
$this->addSql('UPDATE auditlog INNER JOIN contest ON CAST(contest.cid AS CHAR) = auditlog.cid SET auditlog.cid = contest.externalid');
35+
foreach (static::ENTITIES as $table => $fields) {
36+
if (!is_array($fields)) {
37+
$fields = [$fields];
38+
}
39+
foreach ($fields as $field) {
40+
$this->addSql(sprintf(
41+
'UPDATE auditlog INNER JOIN %s ON CAST(%s.%s AS CHAR) = auditlog.dataid AND auditlog.datatype = "%s" SET auditlog.dataid = %s.externalid',
42+
$table, $table, $field, $table, $table
43+
));
44+
}
45+
}
46+
}
47+
48+
public function down(Schema $schema): void
49+
{
50+
// Note: this migration only works for entities that still exist. For others, it will not work but there is nothing we can do
51+
$this->addSql('UPDATE auditlog INNER JOIN contest ON contest.externalid = auditlog.cid SET auditlog.cid = contest.cid');
52+
foreach (static::ENTITIES as $table => $fields) {
53+
$field = is_array($fields) ? $fields[0] : $fields;
54+
$this->addSql(sprintf(
55+
'UPDATE auditlog INNER JOIN %s ON %s.externalid = auditlog.dataid AND auditlog.datatype = "%s" SET auditlog.dataid = %s.%s',
56+
$table, $table, $table, $table, $field
57+
));
58+
}
59+
$this->addSql('ALTER TABLE auditlog CHANGE cid cid INT UNSIGNED DEFAULT NULL COMMENT \'Contest ID associated to this entry\', CHANGE dataid dataid VARCHAR(64) DEFAULT NULL COMMENT \'Identifier in reference table\'');
60+
}
61+
62+
public function isTransactional(): bool
63+
{
64+
return false;
65+
}
66+
}

webapp/public/js/domjudge.js

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -999,14 +999,16 @@ function initializeKeyboardShortcuts() {
999999
var parts = window.location.href.split('/');
10001000
var lastPart = parts[parts.length - 1];
10011001
var params = lastPart.split('?');
1002-
var currentNumber = parseInt(params[0]);
1003-
if (isNaN(currentNumber)) {
1004-
return;
1005-
}
10061002
if (key === 'j') {
1007-
parts[parts.length - 1] = currentNumber + 1;
1003+
if (!window.nextEntity) {
1004+
return;
1005+
}
1006+
parts[parts.length - 1] = window.nextEntity;
10081007
} else if (key === 'k') {
1009-
parts[parts.length - 1] = currentNumber - 1;
1008+
if (!window.previousEntity) {
1009+
return;
1010+
}
1011+
parts[parts.length - 1] = window.previousEntity;
10101012
}
10111013
if (params.length > 1) {
10121014
parts[parts.length - 1] += '?' + params[1];
@@ -1040,9 +1042,13 @@ function initializeKeyboardShortcuts() {
10401042
sequence = '';
10411043
return;
10421044
}
1043-
if (e.key >= '0' && e.key <= '9') {
1045+
if (/^[a-zA-Z0-9_.-]$/.test(e.key)) {
10441046
sequence += e.key;
10451047
box.text(type + sequence);
1048+
} else if (e.key === 'Backspace') {
1049+
e.preventDefault();
1050+
sequence = sequence.slice(0, -1);
1051+
box.text(type + sequence);
10461052
} else {
10471053
ignore = false;
10481054
if (box) {
@@ -1303,8 +1309,8 @@ function initScoreboardSubmissions() {
13031309
const linkEl = e.currentTarget;
13041310
e.preventDefault();
13051311
const $modal = $('[data-submissions-modal] .modal').clone();
1306-
const $teamEl = $(`[data-team-external-id="${linkEl.dataset.teamId}"]`);
1307-
const $problemEl = $(`[data-problem-external-id="${linkEl.dataset.problemId}"]`);
1312+
const $teamEl = $(`tr[data-team-id="${linkEl.dataset.teamId}"]`);
1313+
const $problemEl = $(`th[data-problem-id="${linkEl.dataset.problemId}"]`);
13081314
$modal.find('[data-team]').html($teamEl.data('teamName'));
13091315
$modal.find('[data-problem-badge]').html($problemEl.data('problemBadge'));
13101316
$modal.find('[data-problem-name]').html($problemEl.data('problemName'));
@@ -1404,7 +1410,7 @@ function initDiffEditor(editorId) {
14041410
'onDiffSelectChange': (f) => {
14051411
select.change((e) => {
14061412
const noDiff = e.target.value === "";
1407-
const submitId = parseInt(e.target.value);
1413+
const submitId = e.target.value;
14081414
f(submitId, noDiff);
14091415
});
14101416
}
@@ -1433,10 +1439,10 @@ function initDiffEditor(editorId) {
14331439
diffTitle.style.display = 'inline';
14341440
diffTag.innerText = selected.dataset.tag;
14351441
diffLink.href = selected.dataset.url;
1436-
diffLink.innerText = `s${submitId}`;
1442+
diffLink.innerText = submitId;
14371443
}
14381444
};
1439-
updateSelect(parseInt(select[0].value), select[0].value === "");
1445+
updateSelect(select[0].value, select[0].value === "");
14401446
editor.onDiffSelectChange(updateSelect);
14411447
}
14421448

@@ -1571,7 +1577,7 @@ function initDiffEditorTab(editorId, diffId, submissionId, models) {
15711577
return;
15721578
}
15731579

1574-
const submitId = parseInt(editors[editorId].getDiffSelection());
1580+
const submitId = editors[editorId].getDiffSelection();
15751581
const noDiff = editors[editorId].getDiffSelection() === "";
15761582
if (noDiff) {
15771583
setIcon('file');

webapp/src/Controller/API/BalloonController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ public function listAction(
5959
foreach ($balloonsData as $b) {
6060
/** @var Team $team */
6161
$team = $b['data']['team'];
62-
$teamName = "t" . $team->getTeamid() . ": " . $team->getEffectiveName();
62+
$teamName = $team->getExternalid() . ": " . $team->getEffectiveName();
6363
$balloons[] = new Balloon(
6464
balloonid: $b['data']['balloonid'],
6565
time: $b['data']['time'],
6666
problem: $b['data']['problem'],
6767
contestproblem: $b['data']['contestproblem'],
6868
team: $teamName,
69-
teamid: $team->getTeamid(),
69+
teamid: $team->getExternalid(),
7070
location: $b['data']['location'],
7171
affiliation: $b['data']['affiliation'],
7272
affiliationid: $b['data']['affiliationid'],

0 commit comments

Comments
 (0)