diff --git a/webapp/migrations/Version20231124162457.php b/webapp/migrations/Version20231124162457.php new file mode 100644 index 00000000000..8b99aed5a0b --- /dev/null +++ b/webapp/migrations/Version20231124162457.php @@ -0,0 +1,42 @@ +addSql('CREATE TABLE submission_file_content (submitfileid INT UNSIGNED NOT NULL COMMENT \'Submission file ID\', sourcecode LONGBLOB NOT NULL COMMENT \'Submission file sourcecode(DC2Type:blobtext)\', PRIMARY KEY(submitfileid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Stores contents of submission files\' '); + $this->addSql('ALTER TABLE submission_file_content ADD CONSTRAINT FK_5846B4F7834A5B68 FOREIGN KEY (submitfileid) REFERENCES submission_file (submitfileid) ON DELETE CASCADE'); + $this->addSql('INSERT INTO submission_file_content (submitfileid, sourcecode) SELECT submitfileid, sourcecode FROM submission_file'); + $this->addSql('ALTER TABLE submission_file DROP sourcecode'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE submission_file ADD sourcecode LONGBLOB NOT NULL COMMENT \'Full source code(DC2Type:blobtext)\''); + $this->addSql('UPDATE submission_file INNER JOIN submission_file_content USING (submitfileid) SET submission_file.sourcecode = submission_file_content.sourcecode'); + $this->addSql('ALTER TABLE submission_file_content DROP FOREIGN KEY FK_5846B4F7834A5B68'); + $this->addSql('DROP TABLE submission_file_content'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index dfb75098fcb..e6ec7f986b8 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -1335,7 +1335,8 @@ private function getSourceFiles(string $id): array { $queryBuilder = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'f') - ->select('f') + ->join('f.content', 'c') + ->select('f', 'c') ->andWhere('f.submission = :submitid') ->setParameter('submitid', $id) ->orderBy('f.ranknumber'); @@ -1351,7 +1352,7 @@ private function getSourceFiles(string $id): array foreach ($files as $file) { $result[] = [ 'filename' => $file->getFilename(), - 'content' => base64_encode($file->getSourcecode()), + 'content' => base64_encode($file->getContent()->getSourcecode()), ]; } return $result; diff --git a/webapp/src/Controller/API/SubmissionController.php b/webapp/src/Controller/API/SubmissionController.php index 7da7bcfeb70..391bfc01a6a 100644 --- a/webapp/src/Controller/API/SubmissionController.php +++ b/webapp/src/Controller/API/SubmissionController.php @@ -514,7 +514,8 @@ public function getSubmissionSourceCodeAction(Request $request, string $id): arr $queryBuilder = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'f') ->join('f.submission', 's') - ->select('f, s') + ->join('.f.content', 'c') + ->select('f, s, c') ->andWhere('s.contest = :cid') ->andWhere('s.submitid = :submitid') ->setParameter('cid', $this->getContestId($request)) @@ -534,7 +535,7 @@ public function getSubmissionSourceCodeAction(Request $request, string $id): arr 'id' => (string)$file->getSubmitfileid(), 'submission_id' => (string)$file->getSubmission()->getSubmitid(), 'filename' => $file->getFilename(), - 'source' => base64_encode($file->getSourcecode()), + 'source' => base64_encode($file->getContent()->getSourcecode()), ]; } return $result; diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 65f81d7510a..54f8ab663ad 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -356,7 +356,7 @@ public function exportAction(int $problemId): StreamedResponse $directory = sprintf('submissions/%s/s%d/', $problemResult, $solution->getSubmitid()); /** @var SubmissionFile $source */ foreach ($solution->getFiles() as $source) { - $zip->addFromString($directory . $source->getFilename(), $source->getSourcecode()); + $zip->addFromString($directory . $source->getFilename(), $source->getContent()->getSourcecode()); } } $zip->close(); diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index d92a3e281e8..da33de66ece 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -197,7 +197,7 @@ public function viewAction( ->leftJoin('s.files', 'f') ->leftJoin('s.external_judgements', 'ej', Join::WITH, 'ej.valid = 1') ->leftJoin('s.contest_problem', 'cp') - ->select('s', 't', 'p', 'l', 'c', 'partial f.{submitfileid, filename}', 'cp', 'ej') + ->select('s', 't', 'p', 'l', 'c', 'f', 'cp', 'ej') ->andWhere('s.submitid = :submitid') ->setParameter('submitid', $submitId) ->getQuery() @@ -697,7 +697,8 @@ public function sourceAction( /** @var SubmissionFile|null $file */ $file = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'file') - ->select('file') + ->join('file.content', 'content') + ->select('file, content') ->andWhere('file.ranknumber = :ranknumber') ->andWhere('file.submission = :submission') ->setParameter('ranknumber', $fetch) @@ -713,8 +714,8 @@ public function sourceAction( $response->headers->set('Content-Type', sprintf('text/plain; name="%s"; charset="utf-8"', $file->getFilename())); $response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $file->getFilename())); - $response->headers->set('Content-Length', (string)strlen($file->getSourcecode())); - $response->setContent($file->getSourcecode()); + $response->headers->set('Content-Length', (string)strlen($file->getContent()->getSourcecode())); + $response->setContent($file->getContent()->getSourcecode()); return $response; } @@ -722,7 +723,8 @@ public function sourceAction( /** @var SubmissionFile[] $files */ $files = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'file') - ->select('file') + ->join('file.content', 'content') + ->select('file, content') ->andWhere('file.submission = :submission') ->setParameter('submission', $submission) ->orderBy('file.ranknumber') @@ -738,7 +740,8 @@ public function sourceAction( /** @var SubmissionFile[] $files */ $originalFiles = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'file') - ->select('file') + ->join('file.content', 'content') + ->select('file, content') ->andWhere('file.submission = :submission') ->setParameter('submission', $originalSubmission) ->orderBy('file.ranknumber') @@ -782,7 +785,8 @@ public function sourceAction( /** @var SubmissionFile[] $files */ $oldFiles = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'file') - ->select('file') + ->join('file.content', 'content') + ->select('file, content') ->andWhere('file.submission = :submission') ->setParameter('submission', $oldSubmission) ->orderBy('file.ranknumber') @@ -818,7 +822,8 @@ public function editSourceAction(Request $request, Submission $submission, #[Map /** @var SubmissionFile[] $files */ $files = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'file') - ->select('file') + ->join('file.content', 'content') + ->select('file, content') ->andWhere('file.submission = :submission') ->setParameter('submission', $submission) ->orderBy('file.ranknumber') @@ -832,7 +837,7 @@ public function editSourceAction(Request $request, Submission $submission, #[Map ]; foreach ($files as $file) { - $data['source' . $file->getRank()] = $file->getSourcecode(); + $data['source' . $file->getRank()] = $file->getContent()->getSourcecode(); } $formBuilder = $this->createFormBuilder($data) @@ -1082,6 +1087,10 @@ public function verifyShadowDifferenceAction( return $this->redirectToLocalReferrer($this->router, $request, $redirect); } + /** + * @param SubmissionFile[] $files + * @param SubmissionFile[] $oldFiles + */ protected function determineFileChanged(array $files, array $oldFiles): array { $result = [ @@ -1098,7 +1107,7 @@ protected function determineFileChanged(array $files, array $oldFiles): array foreach ($files as $newfile) { foreach ($oldFiles as $oldFile) { if ($newfile->getFilename() === $oldFile->getFilename()) { - if ($oldFile->getSourcecode() === $newfile->getSourcecode()) { + if ($oldFile->getContent()->getSourcecode() === $newfile->getContent()->getSourcecode()) { $result['unchanged'][] = $newfile->getFilename(); } else { $result['changed'][] = $newfile->getFilename(); diff --git a/webapp/src/Entity/SubmissionFile.php b/webapp/src/Entity/SubmissionFile.php index a23c39f92a1..73cb1ae9ad1 100644 --- a/webapp/src/Entity/SubmissionFile.php +++ b/webapp/src/Entity/SubmissionFile.php @@ -2,6 +2,8 @@ namespace App\Entity; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -36,8 +38,25 @@ class SubmissionFile #[ORM\JoinColumn(name: 'submitid', referencedColumnName: 'submitid', onDelete: 'CASCADE')] private Submission $submission; - #[ORM\Column(type: 'blobtext', options: ['comment' => 'Full source code'])] - private string $sourcecode; + /** + * @var Collection + * + * We use a OneToMany instead of a OneToOne here, because otherwise this + * relation will always be loaded. See the commit message of commit + * 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation + */ + #[ORM\OneToMany( + mappedBy: 'submissionFile', + targetEntity: SubmissionFileContent::class, + cascade: ['persist'], + orphanRemoval: true + )] + private Collection $content; + + public function __construct() + { + $this->content = new ArrayCollection(); + } public function getSubmitfileid(): int { @@ -77,14 +96,17 @@ public function getSubmission(): Submission return $this->submission; } - public function setSourcecode(string $sourcecode): SubmissionFile + public function setContent(SubmissionFileContent $content): self { - $this->sourcecode = $sourcecode; + $this->content->clear(); + $this->content->add($content); + $content->setSubmissionFile($this); + return $this; } - public function getSourcecode(): string + public function getContent(): ?SubmissionFileContent { - return $this->sourcecode; + return $this->content->first() ?: null; } } diff --git a/webapp/src/Entity/SubmissionFileContent.php b/webapp/src/Entity/SubmissionFileContent.php new file mode 100644 index 00000000000..29452f746b7 --- /dev/null +++ b/webapp/src/Entity/SubmissionFileContent.php @@ -0,0 +1,51 @@ + 'utf8mb4_unicode_ci', + 'charset' => 'utf8mb4', + 'comment' => 'Stores contents of submission files', +])] +class SubmissionFileContent +{ + /** + * We use a ManyToOne instead of a OneToOne here, because otherwise the + * reverse of this relation will always be loaded. See the commit message of commit + * 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation. + */ + #[ORM\Id] + #[ORM\ManyToOne(inversedBy: 'content')] + #[ORM\JoinColumn(name: 'submitfileid', referencedColumnName: 'submitfileid', onDelete: 'CASCADE')] + private SubmissionFile $submissionFile; + + #[ORM\Column(type: 'blobtext', options: ['comment' => 'Submission file sourcecode'])] + private string $sourcecode; + + public function getSubmissionFile(): SubmissionFile + { + return $this->submissionFile; + } + + public function setSubmissionFile(SubmissionFile $submissionFile): self + { + $this->submissionFile = $submissionFile; + + return $this; + } + + public function getSourcecode(): string + { + return $this->sourcecode; + } + + public function setSourcecode(string $sourcecode): self + { + $this->sourcecode = $sourcecode; + + return $this; + } +} diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 33b10ce20eb..9b989e6ba7c 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -10,6 +10,7 @@ use App\Entity\Problem; use App\Entity\Submission; use App\Entity\SubmissionFile; +use App\Entity\SubmissionFileContent; use App\Entity\Team; use App\Entity\User; use App\Utils\FreezeData; @@ -610,11 +611,13 @@ public function submitSolution( $this->em->persist($submission); foreach ($files as $rank => $file) { + $submissionFileContent = (new SubmissionFileContent()) + ->setSourcecode(file_get_contents($file->getRealPath())); $submissionFile = new SubmissionFile(); $submissionFile ->setFilename($file->getClientOriginalName()) ->setRank($rank) - ->setSourcecode(file_get_contents($file->getRealPath())); + ->setContent($submissionFileContent); $submissionFile->setSubmission($submission); $this->em->persist($submissionFile); } @@ -767,7 +770,7 @@ public function getSubmissionZipResponse(Submission $submission): StreamedRespon throw new ServiceUnavailableHttpException(null, "Could not create temporary zip file."); } foreach ($files as $file) { - $zip->addFromString($file->getFilename(), $file->getSourcecode()); + $zip->addFromString($file->getFilename(), $file->getContent()->getSourcecode()); } $zip->close(); diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 7b6189ed66e..6e57df56a82 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -877,7 +877,7 @@ protected function parseSourceDiff(string $difftext): string public function showDiff(SubmissionFile $newFile, SubmissionFile $oldFile): string { $differ = new Differ; - return $this->parseSourceDiff($differ->diff($oldFile->getSourcecode(), $newFile->getSourcecode())); + return $this->parseSourceDiff($differ->diff($oldFile->getContent()->getSourcecode(), $newFile->getContent()->getSourcecode())); } public function printContestStart(Contest $contest): string diff --git a/webapp/templates/jury/submission_edit_source.html.twig b/webapp/templates/jury/submission_edit_source.html.twig index fda25e00f1d..2ac827584fe 100644 --- a/webapp/templates/jury/submission_edit_source.html.twig +++ b/webapp/templates/jury/submission_edit_source.html.twig @@ -32,7 +32,7 @@
- {{ file.sourcecode | codeEditor(idx, submission.language.aceLanguage, true, 'form_source' ~ file.rank) }} + {{ file.conte.tsourcecode | codeEditor(idx, submission.language.aceLanguage, true, 'form_source' ~ file.rank) }}