From 8330284c712beb385785a874da14d9643d3bb975 Mon Sep 17 00:00:00 2001 From: Kevin Jilissen Date: Sat, 23 Nov 2024 16:04:12 +0100 Subject: [PATCH 1/4] Define a service function for printing as a user Share some code which is used in multiple controllers, and allow this shared code to be used by API endpoints in the future. --- .../src/Controller/Jury/PrintController.php | 3 +-- webapp/src/Controller/Team/MiscController.php | 18 +++++-------- webapp/src/Service/DOMJudgeService.php | 26 +++++++++++++++++++ 3 files changed, 33 insertions(+), 14 deletions(-) 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/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index f63e31ae3c..14b8f1c117 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -725,6 +725,32 @@ public function openZipFile(string $filename): ZipArchive return $zip; } + /** + * Print the given file using the print command. + * + * Returns array with two elements: first a boolean indicating + * overall success, and second the data returned from the print command. + * + * @param string $filename The on-disk file to be printed out + * @param string $origname The original filename as submitted by the team + * @param string|null $language Langid of the programming language this file is in + * @param bool $asTeam Print the file as the team associated with the user + */ + public function printUserFile( + string $filename, + string $origname, + ?string $language, + ?bool $asTeam = false, + ): array { + $user = $this->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. * From f409cb4d36ba18b23571dcf8c294839aeeb4658f Mon Sep 17 00:00:00 2001 From: Kevin Jilissen Date: Sat, 23 Nov 2024 16:05:53 +0100 Subject: [PATCH 2/4] Define API endpoint for printing as a team This endpoint can be used for `printfile` CLI submit tools, when printing via DOMjudge has been configured. --- webapp/src/Controller/API/PrintController.php | 120 ++++++++++++++++++ webapp/src/DataTransferObject/PrintTeam.php | 19 +++ 2 files changed, 139 insertions(+) create mode 100644 webapp/src/Controller/API/PrintController.php create mode 100644 webapp/src/DataTransferObject/PrintTeam.php 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/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 @@ + Date: Sat, 23 Nov 2024 17:41:26 +0100 Subject: [PATCH 3/4] Allow printing from the submit client To allow easy deployments, have printing and submitting in the same python file such that we can share code without need to import library files. For convenience of teams, you probably want to alias `printfile` to the submit client with predefined `--print` argument to prevent any accidental submission. --- submit/submit | 63 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) 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: From b35ac4d1d17f03f51933fdb7f9da1b5a85978a14 Mon Sep 17 00:00:00 2001 From: Kevin Jilissen Date: Sat, 23 Nov 2024 18:33:09 +0100 Subject: [PATCH 4/4] Adjust integration test for printing API --- .github/workflows/integration.yml | 6 ++++++ submit/submit_online.bats | 9 +++++++++ submit/submit_standalone.bats | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) 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_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 ...]"