Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 60 additions & 3 deletions submit/submit
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#

import argparse
import base64
import datetime
import json
import logging
Expand Down Expand Up @@ -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.'''

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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 '<None>'}'")
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:
Expand Down
9 changes: 9 additions & 0 deletions submit/submit_online.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion submit/submit_standalone.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...]"
Expand Down
120 changes: 120 additions & 0 deletions webapp/src/Controller/API/PrintController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php declare(strict_types=1);

namespace App\Controller\API;

use App\DataTransferObject\PrintTeam;
use App\Service\DOMJudgeService;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use App\Entity\Language;

#[Route(path: "/printing")]
#[OA\Tag(name: "Printing")]
#[OA\Response(ref: "#/components/responses/Unauthenticated", response: 401)]
#[OA\Response(ref: "#/components/responses/Unauthorized", response: 403)]
class PrintController extends AbstractFOSRestController
{
public function __construct(
protected readonly EntityManagerInterface $em,
protected readonly DOMJudgeService $dj,
protected readonly LoggerInterface $logger
) {
}

/**
* Send a file to the system printer as a team.
* @return JsonResponse
*/
#[IsGranted("ROLE_TEAM")]
#[Rest\Post("/team")]
#[OA\Tag(name: "Printing")]
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(ref: new Model(type: PrintTeam::class))
)]
#[OA\Response(
response: 200,
description: "Returns the ID of the imported problem and any messages produced",
content: new OA\JsonContent(
properties: [
new OA\Property(
property: "success",
description: "Whether printing was successful",
type: "boolean"
),
new OA\Property(
property: "output",
description: "The print command output",
type: "string"
),
],
type: "object"
)
)]
#[OA\Response(
response: 422,
description: "An error occurred while processing the print job",
content: new OA\JsonContent(
properties: [
new OA\Property(property: "errors", type: "object"),
]
)
)]
public function printAsTeam(
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
PrintTeam $print,
Request $request
): JsonResponse {
$langid = null;
if ($print->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
);
}
}
3 changes: 1 addition & 2 deletions webapp/src/Controller/Jury/PrintController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
18 changes: 6 additions & 12 deletions webapp/src/Controller/Team/MiscController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
19 changes: 19 additions & 0 deletions webapp/src/DataTransferObject/PrintTeam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types=1);

namespace App\DataTransferObject;

use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;

#[OA\Schema(required: ['name'])]
class PrintTeam
{
public function __construct(
#[OA\Property(description: 'The original name of the file')]
public readonly string $originalName,
#[OA\Property(description: 'The programming language of the file contents', nullable: true)]
public readonly ?string $language,
#[OA\Property(description: 'The (base64-encoded) contents of the source file', format: 'binary')]
public readonly string $fileContents,
) {}
}
26 changes: 26 additions & 0 deletions webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,32 @@
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.
*
Expand Down Expand Up @@ -1491,8 +1517,8 @@
if ($mergeExternal) {
foreach ($this->config->get('external_judgement_types') as $id => $name) {
$verdicts[$name] = $id;
}

Check failure on line 1520 in webapp/src/Service/DOMJudgeService.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 1 space(s) after FOREACH keyword; 0 found

Check failure on line 1520 in webapp/src/Service/DOMJudgeService.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 0 spaces after opening bracket; 1 found

Check failure on line 1520 in webapp/src/Service/DOMJudgeService.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 0 spaces before closing bracket; 1 found
}

Check failure on line 1521 in webapp/src/Service/DOMJudgeService.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 0 spaces after opening bracket; 1 found

Check failure on line 1521 in webapp/src/Service/DOMJudgeService.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected 0 spaces before closing bracket; 1 found

return $verdicts;
}
Expand Down
Loading