Skip to content

Commit 2a947c1

Browse files
Use a Symfony Form for jury clarifications.
Also sanitize only after converting to markdown. Fixes #2311
1 parent 7089c05 commit 2a947c1

File tree

13 files changed

+263
-214
lines changed

13 files changed

+263
-214
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
framework:
2+
html_sanitizer:
3+
sanitizers:
4+
app.clarification_sanitizer:
5+
allow_safe_elements: true
6+
allow_relative_medias: true

webapp/public/js/domjudge.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,10 @@ function toggleExpand(event)
420420
function clarificationAppendAnswer() {
421421
if ( $('#clar_answers').val() == '_default' ) { return; }
422422
var selected = $("#clar_answers option:selected").text();
423-
var textbox = $('#bodytext');
423+
var textbox = $('#jury_clarification_message');
424424
textbox.val(textbox.val().replace(/\n$/, "") + '\n' + selected);
425425
textbox.scrollTop(textbox[0].scrollHeight);
426-
previewClarification($('#bodytext') , $('#messagepreview'));
426+
previewClarification($('#jury_clarification_message') , $('#messagepreview'));
427427
}
428428

429429
function confirmLogout() {

webapp/src/Controller/Jury/ClarificationController.php

Lines changed: 86 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
use App\Entity\Problem;
99
use App\Entity\Team;
1010
use App\Entity\User;
11+
use App\Form\Type\JuryClarificationType;
1112
use App\Service\ConfigurationService;
1213
use App\Service\DOMJudgeService;
1314
use App\Service\EventLogService;
1415
use App\Utils\Utils;
1516
use Doctrine\ORM\EntityManagerInterface;
1617
use Doctrine\ORM\Query\Expr\Join;
17-
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
18+
use Symfony\Component\Form\FormInterface;
1819
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
1920
use Symfony\Component\Security\Http\Attribute\IsGranted;
2021
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -122,32 +123,62 @@ public function indexAction(
122123
}
123124

124125
#[Route(path: '/{id<\d+>}', name: 'jury_clarification')]
125-
public function viewAction(int $id): Response
126+
public function viewAction(Request $request, int $id): Response
126127
{
127128
$clarification = $this->em->getRepository(Clarification::class)->find($id);
128129
if (!$clarification) {
129130
throw new NotFoundHttpException(sprintf('Clarification with ID %s not found', $id));
130131
}
131132

132-
$clardata = ['list'=>[]];
133-
$clardata['clarform'] = $this->getClarificationFormData($clarification->getSender());
134-
$clardata['showExternalId'] = $this->eventLogService->externalIdFieldForEntity(Clarification::class);
135-
136-
$categories = $clardata['clarform']['subjects'];
137-
$queues = $this->config->get('clar_queues');
138-
$clar_answers = $this->config->get('clar_answers');
139-
140133
if ($inReplyTo = $clarification->getInReplyTo()) {
141134
$clarification = $inReplyTo;
142135
}
143-
$clarlist = [$clarification];
136+
$clarificationList = [$clarification];
144137
$replies = $clarification->getReplies();
145-
foreach ($replies as $clar_reply) {
146-
$clarlist[] = $clar_reply;
138+
foreach ($replies as $reply) {
139+
$clarificationList[] = $reply;
140+
}
141+
142+
$parameters = ['list' => []];
143+
$parameters['showExternalId'] = $this->eventLogService->externalIdFieldForEntity(Clarification::class);
144+
145+
$formData = [
146+
'recipient' => JuryClarificationType::RECIPIENT_MUST_SELECT,
147+
'subject' => sprintf('%s-%s', $clarification->getContest()->getCid(), $clarification->getProblem()?->getProbid() ?? $clarification->getCategory()),
148+
];
149+
if ($clarification->getRecipient()) {
150+
$formData['recipient'] = $clarification->getRecipient()->getTeamid();
151+
}
152+
153+
/** @var Clarification $lastClarification */
154+
$lastClarification = end($clarificationList);
155+
$formData['message'] = "> " . str_replace("\n", "\n> ", Utils::wrapUnquoted($lastClarification->getBody())) . "\n\n";
156+
157+
$form = $this->createForm(JuryClarificationType::class, $formData, ['limit_to_team' => $clarification->getSender()]);
158+
159+
$form->handleRequest($request);
160+
161+
if ($form->isSubmitted() && $form->isValid()) {
162+
return $this->processSubmittedClarification($form, $clarification);
163+
}
164+
165+
$parameters['form'] = $form->createView();
166+
167+
$categories = array_flip($form->get('subject')->getConfig()->getOptions()['choices']);
168+
$groupedCategories = [];
169+
foreach ($categories as $key => $value) {
170+
if ($this->dj->getCurrentContest()) {
171+
$groupedCategories[$this->dj->getCurrentContest()->getShortname()][$key] = $value;
172+
} else {
173+
[$group] = explode(' - ', $value, 2);
174+
$groupedCategories[$group][$key] = $value;
175+
}
147176
}
177+
$parameters['subjects'] = $groupedCategories;
178+
$queues = $this->config->get('clar_queues');
179+
$clarificationAnswers = $this->config->get('clar_answers');
148180

149-
$concernsteam = null;
150-
foreach ($clarlist as $clar) {
181+
foreach ($clarificationList as $clar) {
151182
$data = ['clarid' => $clar->getClarid(), 'externalid' => $clar->getExternalid()];
152183
$data['time'] = $clar->getSubmittime();
153184

@@ -161,7 +192,6 @@ public function viewAction(int $id): Response
161192
if ($fromteam = $clar->getSender()) {
162193
$data['from_teamname'] = $fromteam->getEffectiveName();
163194
$data['from_team'] = $fromteam;
164-
$concernsteam = $fromteam->getTeamid();
165195
}
166196
if ($toteam = $clar->getRecipient()) {
167197
$data['to_teamname'] = $toteam->getEffectiveName();
@@ -181,7 +211,7 @@ public function viewAction(int $id): Response
181211
$concernssubject = "";
182212
}
183213
if ($concernssubject !== "") {
184-
$data['subject'] = $categories[$clarcontest][$concernssubject];
214+
$data['subject'] = $categories[$concernssubject];
185215
} else {
186216
$data['subject'] = $clarcontest;
187217
}
@@ -192,104 +222,36 @@ public function viewAction(int $id): Response
192222
$data['answered'] = $clar->getAnswered();
193223

194224
$data['body'] = $clar->getBody();
195-
$clardata['list'][] = $data;
196-
}
197-
198-
if ($concernsteam) {
199-
$clardata['clarform']['toteam'] = $concernsteam;
200-
}
201-
if ($concernssubject) {
202-
$clardata['clarform']['onsubject'] = $concernssubject;
203-
}
204-
205-
$clardata['clarform']['quotedtext'] = "> " . str_replace("\n", "\n> ", Utils::wrapUnquoted($data['body'])) . "\n\n";
206-
$clardata['clarform']['queues'] = $queues;
207-
$clardata['clarform']['answers'] = $clar_answers;
208-
209-
return $this->render('jury/clarification.html.twig',
210-
$clardata
211-
);
212-
}
213-
214-
/**
215-
* @return array{teams: array<string|int, string>, subjects: array<string, array<string, string>>}
216-
*/
217-
protected function getClarificationFormData(?Team $team = null): array
218-
{
219-
$teamlist = [];
220-
$em = $this->em;
221-
if ($team !== null) {
222-
$teamlist[$team->getTeamid()] = sprintf("%s (t%s)", $team->getEffectiveName(), $team->getTeamid());
223-
} else {
224-
$teams = $em->getRepository(Team::class)->findAll();
225-
foreach ($teams as $team) {
226-
$teamlist[$team->getTeamid()] = sprintf("%s (t%s)", $team->getEffectiveName(), $team->getTeamid());
227-
}
228-
}
229-
asort($teamlist, SORT_STRING | SORT_FLAG_CASE);
230-
$teamlist = ['domjudge-must-select' => '(select...)', '' => 'ALL'] + $teamlist;
231-
232-
$data= ['teams' => $teamlist ];
233-
234-
$subject_options = [];
235-
236-
$categories = $this->config->get('clar_categories');
237-
$contest = $this->dj->getCurrentContest();
238-
$hasCurrentContest = $contest !== null;
239-
if ($hasCurrentContest) {
240-
$contests = [$contest->getCid() => $contest];
241-
} else {
242-
$contests = $this->dj->getCurrentContests();
243-
}
244-
245-
/** @var ContestProblem[] $contestproblems */
246-
$contestproblems = $this->em->createQueryBuilder()
247-
->from(ContestProblem::class, 'cp')
248-
->select('cp, p')
249-
->innerJoin('cp.problem', 'p')
250-
->where('cp.contest IN (:contests)')
251-
->setParameter('contests', $contests)
252-
->orderBy('cp.shortname')
253-
->getQuery()->getResult();
254-
255-
foreach ($contests as $cid => $cdata) {
256-
$cshort = $cdata->getShortName();
257-
$namePrefix = '';
258-
if (!$hasCurrentContest) {
259-
$namePrefix = $cshort . ' - ';
260-
}
261-
foreach ($categories as $name => $desc) {
262-
$subject_options[$cshort]["$cid-$name"] = "$namePrefix $desc";
263-
}
264-
265-
foreach ($contestproblems as $cp) {
266-
if ($cp->getCid()!=$cid) {
267-
continue;
268-
}
269-
$subject_options[$cshort]["$cid-" . $cp->getProbid()] =
270-
$namePrefix . $cp->getShortname() . ': ' . $cp->getProblem()->getName();
271-
}
225+
$parameters['list'][] = $data;
272226
}
273227

274-
$data['subjects'] = $subject_options;
228+
$parameters['queues'] = $queues;
229+
$parameters['answers'] = $clarificationAnswers;
275230

276-
return $data;
231+
return $this->render('jury/clarification.html.twig', $parameters);
277232
}
278233

279-
#[Route(path: '/send', methods: ['GET'], name: 'jury_clarification_new')]
234+
#[Route(path: '/send', name: 'jury_clarification_new')]
280235
public function composeClarificationAction(
236+
Request $request,
281237
#[MapQueryParameter]
282-
?string $teamto = null
238+
?string $teamto = null,
283239
): Response {
284-
// TODO: Use a proper Symfony form for this.
285-
286-
$data = $this->getClarificationFormData();
240+
$formData = ['recipient' => JuryClarificationType::RECIPIENT_MUST_SELECT];
287241

288242
if ($teamto !== null) {
289-
$data['toteam'] = $teamto;
243+
$formData['recipient'] = $teamto;
244+
}
245+
246+
$form = $this->createForm(JuryClarificationType::class, $formData);
247+
248+
$form->handleRequest($request);
249+
250+
if ($form->isSubmitted() && $form->isValid()) {
251+
return $this->processSubmittedClarification($form);
290252
}
291253

292-
return $this->render('jury/clarification_new.html.twig', ['clarform' => $data]);
254+
return $this->render('jury/clarification_new.html.twig', ['form' => $form->createView()]);
293255
}
294256

295257
#[Route(path: '/{clarId<\d+>}/claim', name: 'jury_clarification_claim')]
@@ -387,30 +349,25 @@ public function changeQueueAction(Request $request, int $clarId): Response
387349
return $this->redirectToRoute('jury_clarification', ['id' => $clarId]);
388350
}
389351

390-
#[Route(path: '/send', methods: ['POST'], name: 'jury_clarification_send')]
391-
public function sendAction(Request $request, HtmlSanitizerInterface $htmlSanitizer): Response
392-
{
352+
protected function processSubmittedClarification(
353+
FormInterface $form,
354+
?Clarification $inReplTo = null
355+
): Response {
356+
$formData = $form->getData();
393357
$clarification = new Clarification();
358+
$clarification->setInReplyTo($inReplTo);
394359

395-
if ($respid = $request->request->get('id')) {
396-
$respclar = $this->em->getRepository(Clarification::class)->find($respid);
397-
$clarification->setInReplyTo($respclar);
398-
}
399-
400-
$sendto = $request->request->get('sendto');
401-
if (empty($sendto)) {
402-
$sendto = null;
403-
} elseif ($sendto === 'domjudge-must-select') {
404-
$message = 'You must select somewhere to send the clarification to.';
405-
$this->addFlash('danger', $message);
406-
return $this->redirectToRoute('jury_clarification_send');
360+
$recipient = $formData['recipient'];
361+
if (empty($recipient)) {
362+
$recipient = null;
407363
} else {
408-
$team = $this->em->getReference(Team::class, $sendto);
364+
$team = $this->em->getReference(Team::class, $recipient);
409365
$clarification->setRecipient($team);
410366
}
411367

412-
$problem = $request->request->get('problem');
413-
[$cid, $probid] = explode('-', $problem);
368+
369+
$subject = $formData['subject'];
370+
[$cid, $probid] = explode('-', $subject);
414371

415372
$contest = $this->em->getReference(Contest::class, $cid);
416373
$clarification->setContest($contest);
@@ -428,8 +385,8 @@ public function sendAction(Request $request, HtmlSanitizerInterface $htmlSanitiz
428385
}
429386
}
430387

431-
if ($respid) {
432-
$queue = $respclar->getQueue();
388+
if ($inReplTo) {
389+
$queue = $inReplTo->getQueue();
433390
} else {
434391
$queue = $this->config->get('clar_default_problem_queue');
435392
if ($queue === "") {
@@ -440,14 +397,13 @@ public function sendAction(Request $request, HtmlSanitizerInterface $htmlSanitiz
440397

441398
$clarification->setJuryMember($this->getUser()->getUserIdentifier());
442399
$clarification->setAnswered(true);
443-
$clarification->setBody($htmlSanitizer->sanitize($request->request->get('bodytext')));
400+
$clarification->setBody($formData['message']);
444401
$clarification->setSubmittime(Utils::now());
445402

446403
$this->em->persist($clarification);
447-
if ($respid) {
448-
$respclar->setAnswered(true);
449-
$respclar->setJuryMember($this->getUser()->getUserIdentifier());
450-
$this->em->persist($respclar);
404+
if ($inReplTo) {
405+
$inReplTo->setAnswered(true);
406+
$inReplTo->setJuryMember($this->getUser()->getUserIdentifier());
451407
}
452408
$this->em->flush();
453409

@@ -457,9 +413,8 @@ public function sendAction(Request $request, HtmlSanitizerInterface $htmlSanitiz
457413
// Reload clarification to make sure we have a fresh one after calling the event log service.
458414
$clarification = $this->em->getRepository(Clarification::class)->find($clarId);
459415

460-
if ($sendto) {
461-
$team = $this->em->getRepository(Team::class)->find($sendto);
462-
$team->addUnreadClarification($clarification);
416+
if ($clarification->getRecipient()) {
417+
$clarification->getRecipient()->addUnreadClarification($clarification);
463418
} else {
464419
$teams = $this->em->getRepository(Team::class)->findAll();
465420
foreach ($teams as $team) {

webapp/src/Controller/Jury/TeamController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public function indexAction(): Response
167167
$teamactions[] = [
168168
'icon' => 'envelope',
169169
'title' => 'send clarification to this team',
170-
'link' => $this->generateUrl('jury_clarification_send', [
170+
'link' => $this->generateUrl('jury_clarification_new', [
171171
'teamto' => $t->getTeamId(),
172172
])
173173
];

webapp/src/Controller/RootController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ public function markdownPreview(
4545
Request $request,
4646
#[Autowire(service: 'twig.runtime.markdown')]
4747
MarkdownRuntime $markdownRuntime,
48-
HtmlSanitizerInterface $htmlSanitizer
48+
HtmlSanitizerInterface $appClarificationSanitizer,
4949
): JsonResponse {
5050
$message = $request->request->get('message');
5151
if ($message === null) {
5252
throw new BadRequestHttpException('A message is required');
5353
}
54-
return new JsonResponse(['html' => $markdownRuntime->convert($htmlSanitizer->sanitize($message))]);
54+
return new JsonResponse(['html' => $appClarificationSanitizer->sanitize($markdownRuntime->convert($message))]);
5555
}
5656
}

0 commit comments

Comments
 (0)