diff --git a/.github/workflows/autoconf-check-different-distro.yml b/.github/workflows/autoconf-check-different-distro.yml index 8216eb3a26b..9db54dfaa8d 100644 --- a/.github/workflows/autoconf-check-different-distro.yml +++ b/.github/workflows/autoconf-check-different-distro.yml @@ -20,7 +20,7 @@ jobs: image: ${{ matrix.os }}:${{ matrix.version }} steps: - name: Install git so we get the .github directory - run: dnf install -y git + run: dnf install -y git composer - uses: actions/checkout@v4 - name: Setup image and run bats tests run: .github/jobs/configure-checks/setup_configure_image.sh diff --git a/.github/workflows/autoconf-check.yml b/.github/workflows/autoconf-check.yml index 016ec024f71..58f6f691d10 100644 --- a/.github/workflows/autoconf-check.yml +++ b/.github/workflows/autoconf-check.yml @@ -32,7 +32,7 @@ jobs: image: ${{ matrix.os }}:${{ matrix.version }} steps: - name: Install git so we get the .github directory - run: apt-get update; apt-get install -y git + run: apt-get update; apt-get install -y git composer - uses: actions/checkout@v4 - name: Setup image and run bats tests run: .github/jobs/configure-checks/setup_configure_image.sh diff --git a/etc/db-config.yaml b/etc/db-config.yaml index 665a48a6e83..7872110f031 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -201,6 +201,14 @@ - category: Display description: Options related to the DOMjudge user interface. items: + - name: default_submission_code_mode + type: int + default_value: 0 + public: true + description: Select the default submission method for the team + options: + 0: Paste + 1: Upload - name: output_display_limit type: int default_value: 2000 diff --git a/lib/Makefile b/lib/Makefile index 238684c01f0..2e8ce1c216d 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -20,3 +20,5 @@ install-domserver: install-judgehost: $(INSTALL_DATA) -t $(DESTDIR)$(judgehost_libdir) *.php *.sh $(INSTALL_PROG) -t $(DESTDIR)$(judgehost_libdir) alert + +domserver: SUBDIRS=vendor diff --git a/lib/vendor/Makefile b/lib/vendor/Makefile new file mode 100644 index 00000000000..b6ee26a3613 --- /dev/null +++ b/lib/vendor/Makefile @@ -0,0 +1,12 @@ +ifndef TOPDIR +TOPDIR=../.. +endif +include $(TOPDIR)/Makefile.global + +clean-l: + rm -f autoload_runtime.php + +autoload_runtime.php: + composer $(subst 1,-q,$(QUIET)) dump-autoload -o -a -d $(TOPDIR) + +domserver: autoload_runtime.php diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index 35e1a382e7c..734fbd74a97 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -9,6 +9,7 @@ use App\Entity\Submission; use App\Entity\Testcase; use App\Form\Type\SubmitProblemType; +use App\Form\Type\SubmitProblemPasteType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -60,32 +61,48 @@ public function createAction(Request $request, ?Problem $problem = null): Respon if ($problem !== null) { $data['problem'] = $problem; } - $form = $this->formFactory + $formUpload = $this->formFactory ->createBuilder(SubmitProblemType::class, $data) ->setAction($this->generateUrl('team_submit')) ->getForm(); - $form->handleRequest($request); + $formPaste = $this->formFactory + ->createBuilder(SubmitProblemPasteType::class, $data) + ->setAction($this->generateUrl('team_submit')) + ->getForm(); - if ($form->isSubmitted() && $form->isValid()) { + $formUpload->handleRequest($request); + $formPaste->handleRequest($request); + if ($formUpload->isSubmitted() && $formUpload->isValid()) { if ($contest === null) { $this->addFlash('danger', 'No active contest'); } elseif (!$this->dj->checkrole('jury') && !$contest->getFreezeData()->started()) { $this->addFlash('danger', 'Contest has not yet started'); } else { /** @var Problem $problem */ - $problem = $form->get('problem')->getData(); + $problem = $formUpload->get('problem')->getData(); /** @var Language $language */ - $language = $form->get('language')->getData(); + $language = $formUpload->get('language')->getData(); /** @var UploadedFile[] $files */ - $files = $form->get('code')->getData(); + $files = $formUpload->get('code')->getData(); if (!is_array($files)) { $files = [$files]; } - $entryPoint = $form->get('entry_point')->getData() ?: null; + $entryPoint = $formUpload->get('entry_point')->getData() ?: null; $submission = $this->submissionService->submitSolution( - $team, $this->dj->getUser(), $problem->getProbid(), $contest, $language, $files, 'team page', null, - null, $entryPoint, null, null, $message + $team, + $this->dj->getUser(), + $problem->getProbid(), + $contest, + $language, + $files, + 'team page', + null, + null, + $entryPoint, + null, + null, + $message ); if ($submission) { @@ -96,11 +113,77 @@ public function createAction(Request $request, ?Problem $problem = null): Respon } else { $this->addFlash('danger', $message); } + return $this->redirectToRoute('team_index'); + } + } elseif ($formPaste->isSubmitted() && $formPaste->isValid()) { + if ($contest === null) { + $this->addFlash('danger', 'No active contest'); + } elseif (!$this->dj->checkrole('jury') && !$contest->getFreezeData()->started()) { + $this->addFlash('danger', 'Contest has not yet started'); + } else { + $problem = $formPaste->get('problem')->getData(); + $language = $formPaste->get('language')->getData(); + $codeContent = $formPaste->get('code_content')->getData(); + if($codeContent == null || empty(trim($codeContent))) { + $this->addFlash('danger','No code content provided.'); + return $this->redirectToRoute('team_index'); + } + $tempDir = sys_get_temp_dir(); + $tempFileName = sprintf( + 'submission_%s_%s_%s.%s', + $user->getUsername(), + $problem->getName(), + date('Y-m-d_H-i-s'), + $language->getExtensions()[0] + ); + $tempFileName = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $tempFileName); + $tempFilePath = $tempDir . DIRECTORY_SEPARATOR . $tempFileName; + file_put_contents($tempFilePath, $codeContent); + + $uploadedFile = new UploadedFile( + $tempFilePath, + $tempFileName, + 'application/octet-stream', + null, + true + ); + + $files = [$uploadedFile]; + $entryPoint = $tempFileName; + $submission = $this->submissionService->submitSolution( + $team, + $this->dj->getUser(), + $problem, + $contest, + $language, + $files, + 'team page', + null, + null, + $entryPoint, + null, + null, + $message + ); + if ($submission) { + $this->addFlash( + 'success', + 'Submission done! Watch for the verdict in the list below.' + ); + } else { + $this->addFlash('danger', $message); + } + return $this->redirectToRoute('team_index'); } } - $data = ['form' => $form->createView(), 'problem' => $problem]; + $data = [ + 'formupload' => $formUpload->createView(), + 'formpaste' => $formPaste->createView(), + 'problem' => $problem, + 'defaultSubmissionCodeMode' => (bool) $this->config->get('default_submission_code_mode'), + ]; $data['validFilenameRegex'] = SubmissionService::FILENAME_REGEX; if ($request->isXmlHttpRequest()) { diff --git a/webapp/src/Form/Type/SubmitProblemPasteType.php b/webapp/src/Form/Type/SubmitProblemPasteType.php new file mode 100644 index 00000000000..71050b95a8c --- /dev/null +++ b/webapp/src/Form/Type/SubmitProblemPasteType.php @@ -0,0 +1,81 @@ +dj->getUser(); + $contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid()); + + $builder->add('code_content', HiddenType::class, [ + 'required' => true, + ]); + $problemConfig = [ + 'class' => Problem::class, + 'query_builder' => fn(EntityRepository $er) => $er->createQueryBuilder('p') + ->join('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest') + ->select('p', 'cp') + ->andWhere('cp.allowSubmit = 1') + ->setParameter('contest', $contest) + ->addOrderBy('cp.shortname'), + 'choice_label' => fn(Problem $problem) => sprintf( + '%s - %s', + $problem->getContestProblems()->first()->getShortName(), + $problem->getName() + ), + 'placeholder' => 'Select a problem', + ]; + $builder->add('problem', EntityType::class, $problemConfig); + + $builder->add('language', EntityType::class, [ + 'class' => Language::class, + 'query_builder' => fn(EntityRepository $er) => $er + ->createQueryBuilder('l') + ->andWhere('l.allowSubmit = 1'), + 'choice_label' => 'name', + 'placeholder' => 'Select a language', + ]); + + $builder->add('entry_point', TextType::class, [ + 'label' => 'Entry point', + 'required' => false, + 'help' => 'The entry point for your code.', + 'row_attr' => ['data-entry-point' => ''] + ]); + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($problemConfig) { + $data = $event->getData(); + if (isset($data['problem'])) { + $problemConfig += ['row_attr' => ['class' => 'd-none']]; + $event->getForm()->add('problem', EntityType::class, $problemConfig); + } + }); + } +} \ No newline at end of file diff --git a/webapp/templates/base.html.twig b/webapp/templates/base.html.twig index 895750ede82..6ccf9845de3 100644 --- a/webapp/templates/base.html.twig +++ b/webapp/templates/base.html.twig @@ -14,6 +14,8 @@ + + {% for file in customAssetFiles('js') %} {% endfor %} diff --git a/webapp/templates/team/submit_modal.html.twig b/webapp/templates/team/submit_modal.html.twig index cf72bb3119c..ebb459ce699 100644 --- a/webapp/templates/team/submit_modal.html.twig +++ b/webapp/templates/team/submit_modal.html.twig @@ -19,21 +19,64 @@ {% include 'partials/alert.html.twig' with {'type': 'danger', 'message': 'Submissions (temporarily) disabled.'} %} {% else %} - {{ form_start(form) }} + {% set active_tab = defaultSubmissionCodeMode == 0 ? 'paste' : 'upload' %} + +