diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 56ee842a2c..e9d9d0c651 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -42,6 +42,12 @@ jobs: run: sudo misc-tools/dj_make_chroot -a amd64 - name: Check nginx run: curl -v https://localhost/domjudge/ + - name: Configure print command + working-directory: submit + run: | + curl --fail -u 'admin:password' -X 'GET' 'http://localhost/domjudge/api/v4/config?strict=false' \ + | jq '.print_command |= "cp [file] /tmp/dj-printfile"' \ + | curl --fail -u 'admin:password' -X 'PUT' -T - 'http://localhost/domjudge/api/v4/config?strict=false' \ - name: Testing submit client working-directory: submit run: make check-full diff --git a/submit/submit b/submit/submit index 69979d21bb..0db1ccbfdd 100755 --- a/submit/submit +++ b/submit/submit @@ -8,6 +8,7 @@ # import argparse +import base64 import datetime import json import logging @@ -299,6 +300,56 @@ Submit multiple files (the problem and language are taken from the first): return "\n\n".join(part for part in epilog_parts if part) +def do_api_print(): + '''Submit to the API for printing with the given data.''' + + if len(filenames) != 1: + error('You can only print a single file') + filename = filenames[0] + + with open(filename, 'rb') as file: + data = { + 'original_name': filename, + 'file_contents': base64.b64encode(file.read()), + } + if my_language: + data['language'] = my_language['name'] + if entry_point: + data['entry_point'] = entry_point + + url = f"{baseurl}api/{api_version}printing/team" + logging.info(f'connecting to {url}') + + response = requests.post(url, data=data, headers=headers) + + logging.debug(f"API call 'printing' returned:\n{response.text}") + + # The connection worked, but we may have received an HTTP error + if response.status_code >= 300: + print(response.text) + if response.status_code == 401: + raise RuntimeError('Authentication failed, please check your DOMjudge credentials in ~/.netrc.') + else: + raise RuntimeError(f'Printing failed (code {response.status_code})') + + # We got a successful HTTP response. It worked. + # But check that we indeed received a success response. + + try: + result = json.loads(response.text) + except json.decoder.JSONDecodeError as e: + error(f'Parsing DOMjudge\'s API output failed: {e}') + + if not isinstance(result, dict) or 'success' not in result: + error('DOMjudge\'s API returned unexpected JSON data.') + + if result['success']: + print("DOMjudge reported a successful print job.") + else: + # Should not happen, as the status code should've been >= 300 + print(f"DOMjudge reported a printing error: {result['output']}") + + def do_api_submit(): '''Submit to the API with the given data.''' @@ -374,6 +425,7 @@ parser.add_argument('-c', '--contest', help='''submit for contest with ID or sho Defaults to the value of the environment variable 'SUBMITCONTEST'. Mandatory when more than one contest is active.''') +parser.add_argument('-P', '--print', help='submit the file for printing instead of submission', action='store_true') parser.add_argument('-p', '--problem', help='submit for problem with ID or label PROBLEM', default='') parser.add_argument('-l', '--language', help='submit in language with ID LANGUAGE', default='') parser.add_argument('-e', '--entry_point', help='set an explicit entry_point, e.g. the java main class') @@ -530,7 +582,7 @@ for language in languages: if my_language: break -if not my_language: +if not my_language and not args.print: usage('No known language specified or detected.') # Check for problem matching ID or label. @@ -540,7 +592,7 @@ for problem in problems: my_problem = problem break -if not my_problem: +if not my_problem and not args.print: usage('No known problem specified or detected.') # Guess entry point if not already specified. @@ -556,11 +608,16 @@ if not entry_point and my_language['entry_point_required']: error('Entry point required but not specified nor detected.') logging.debug(f"contest is `{my_contest['shortname']}'") -logging.debug(f"problem is `{my_problem['label']}'") +if not args.print: + logging.debug(f"problem is `{my_problem['label']}'") logging.debug(f"language is `{my_language['name']}'") logging.debug(f"entry_point is `{entry_point or ''}'") logging.debug(f"url is `{baseurl}'") +if args.print: + do_api_print() + exit(0) + if not args.assume_yes: print('Submission information:') if len(filenames) == 1: diff --git a/submit/submit_online.bats b/submit/submit_online.bats index 1177854542..26b3179109 100755 --- a/submit/submit_online.bats +++ b/submit/submit_online.bats @@ -136,3 +136,12 @@ setup() { assert_regex "Submission received: id = s[0-9]*, time = [0-9]{2}:[0-9]{2}:[0-9]{2}" assert_regex "Check http[^ ]*/[0-9]* for the result." } + +@test "submit print job" { + run ./submit -P -l C ../example_problems/hello/submissions/accepted/test-hello.c + assert_success + assert_regex "DOMjudge reported a successful print job." + run diff /tmp/dj-printfile ../example_problems/hello/submissions/accepted/test-hello.c + # Diff has exit code 0 iff the files are equal + assert_success +} diff --git a/submit/submit_standalone.bats b/submit/submit_standalone.bats index dd476fd6e8..dcaffe5250 100755 --- a/submit/submit_standalone.bats +++ b/submit/submit_standalone.bats @@ -40,7 +40,7 @@ setup() { @test "display basic usage information" { run ./submit --help assert_success - assert_line "usage: submit [--version] [-h] [-c CONTEST] [-p PROBLEM] [-l LANGUAGE] [-e ENTRY_POINT]" + assert_line "usage: submit [--version] [-h] [-c CONTEST] [-P] [-p PROBLEM] [-l LANGUAGE] [-e ENTRY_POINT]" assert_line " [-v [{DEBUG,INFO,WARNING,ERROR,CRITICAL}]] [-q] [-y] [-u URL]" # The help printer does print this differently on versions of argparse for nargs=*. assert_regex " (filename )?[filename ...]" diff --git a/webapp/src/Controller/API/PrintController.php b/webapp/src/Controller/API/PrintController.php new file mode 100644 index 0000000000..ca4b0fb879 --- /dev/null +++ b/webapp/src/Controller/API/PrintController.php @@ -0,0 +1,120 @@ +language !== null) { + $langid = $this->em + ->createQueryBuilder() + ->from(Language::class, "l") + ->select("l.langid") + ->andWhere("l.allowSubmit = 1") + ->andWhere("l.name = :name") + ->setParameter("name", $print->language) + ->getQuery() + ->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR); + if ($langid === null) { + throw new BadRequestHttpException("Programming language not found."); + } + } + + $decodedFile = base64_decode($print->fileContents, true); + if ($decodedFile === false) { + throw new BadRequestHttpException("The file contents is not base64 encoded."); + } + + if (!($tempFilename = tempnam($this->dj->getDomjudgeTmpDir(), "printfile-"))) { + throw new ServiceUnavailableHttpException(null, "Could not create temporary file."); + } + + if (file_put_contents($tempFilename, $decodedFile) === false) { + throw new ServiceUnavailableHttpException( + null, + sprintf("Could not write printfile to temporary file '%s'.", $tempFilename) + ); + } + + $ret = $this->dj->printUserFile($tempFilename, $print->originalName, $langid, true); + unlink($tempFilename); + + return new JsonResponse( + ["success" => $ret[0], "output" => $ret[1]], + $ret[0] ? Response::HTTP_OK : Response::HTTP_SERVICE_UNAVAILABLE + ); + } +} diff --git a/webapp/src/Controller/Jury/PrintController.php b/webapp/src/Controller/Jury/PrintController.php index 26bb541657..94d1991d15 100644 --- a/webapp/src/Controller/Jury/PrintController.php +++ b/webapp/src/Controller/Jury/PrintController.php @@ -51,11 +51,10 @@ public function showAction(Request $request): Response $originalfilename = $file->getClientOriginalName(); $langid = $data['langid']; - $username = $this->getUser()->getUserIdentifier(); // Since this is the Jury interface, there's not necessarily a // team involved; do not set a teamname or location. - $ret = $this->dj->printFile($realfile, $originalfilename, $langid, $username); + $ret = $this->dj->printUserFile($realfile, $originalfilename, $langid); return $this->render('jury/print_result.html.twig', [ 'success' => $ret[0], diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index aae0630121..f00427db0f 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -176,18 +176,12 @@ public function printAction(Request $request): Response $realfile = $file->getRealPath(); $originalfilename = $file->getClientOriginalName(); - $langid = $data['langid']; - $username = $this->getUser()->getUserIdentifier(); - - $propertyAccessor = PropertyAccess::createPropertyAccessor(); - $team = $this->dj->getUser()->getTeam(); - if ($team->getLabel()) { - $teamId = $team->getLabel(); - } else { - $teamId = $team->getExternalid(); - } - $ret = $this->dj->printFile($realfile, $originalfilename, $langid, - $username, $team->getEffectiveName(), $teamId, $team->getLocation()); + $langid = $data['langid']; + $ret = $this->dj->printUserFile( + $realfile, + $originalfilename, + $langid + ); return $this->render('team/print_result.html.twig', [ 'success' => $ret[0], diff --git a/webapp/src/DataTransferObject/PrintTeam.php b/webapp/src/DataTransferObject/PrintTeam.php new file mode 100644 index 0000000000..fac46802d5 --- /dev/null +++ b/webapp/src/DataTransferObject/PrintTeam.php @@ -0,0 +1,19 @@ +getUser(); + $team = $user->getTeam(); + if ($asTeam && $team !== null) { + $teamid = $team->getLabel() ?? $team->getExternalid(); + return $this->printFile($filename, $origname, $language, $user->getUserIdentifier(), $team->getEffectiveName(), $teamid, $team->getLocation()); + } + return $this->printFile($filename, $origname, $language, $user->getUserIdentifier()); + } + /** * Print the given file using the print command. *