diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e4844362a..9e4b2665b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ contributors. We use the [GitHub issue tracker](https://github.com/DOMjudge/domjudge/issues) for bug reports, feature requests and anything in between. Try to provide a clear story, and fill in as much information as possible in our -[GitHub issue template](https://github.com/DOMjudge/domjudge/blob/master/ISSUE_TEMPLATE.md). +[GitHub issue template](https://github.com/DOMjudge/domjudge/tree/main/.github/ISSUE_TEMPLATE). Providing accurate and relevant details will typically speed up the triaging and debugging process. @@ -51,8 +51,8 @@ If you are interested in starting to contribute to DOMjudge, you are most welcome. The following are some pointers. First, install and configure the development version of DOMjudge. -Either directly from the git repository, see the [developer information](https://www.domjudge.org/docs/manual/master/develop.html) -section in the admin manual, or via the [contributor docker image](https://github.com/DOMjudge/domjudge-packaging/tree/master/docker-contributor). +Either directly from the git repository, see the [developer information](https://www.domjudge.org/docs/manual/main/develop.html) +section in the admin manual, or via the [contributor docker image](https://github.com/DOMjudge/domjudge-packaging/tree/main/docker-contributor). If you follow the admin manual for installation, this is also a great way to verify that the documentation is clear and complete, and to report anything that is not. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ab7a5fde37..0e82db51fa 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -64,7 +64,7 @@ Many people have contributed over the years. Many thanks: Some code has been ported from the ETH Zurich fork by Christoph Krautz, Thomas Rast et al. -The file (CONTRIBUTING.md)[CONTRIBUTING.md] has instructions on +The file [CONTRIBUTING.md](CONTRIBUTING.md) has instructions on how to best contribute. ## Special thanks diff --git a/ChangeLog b/ChangeLog index 2b0b2e7924..8cccb0856f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,29 @@ DOMjudge Programming Contest Judging System +Version 8.2.3 - 28 February 2023 +-------------------------------- + - [security] Close metadata file descriptor for the child in runguard. + - Various documentation & bugfixes. + +Version 8.2.2 - 20 October 2023 +------------------------------- + - Bugfix to prevent errors when running a bare-install or deleting all + contests. + +Version 8.2.1 - 29 May 2023 +--------------------------- + - Set the default domjudge_user to the current user at configure time. + - Various bugfixes. + - Adhere to CLICS 2022-07 again. + - Prevent overwriting of local .netrc when installing examples. + - Fixed some w3c html violations. + - Clarify the need for cgroupv1 when chroot creating fails. + - Fix medal awards when skipping categories. + - Do not allow to delete problems from a locked contest in the UI. + - Disallow submitting to a problem when it does not have testcases at import. + - Fix handling of binary files when editing executables. + - Recalculate all immutable hashes since the algorithm changed in the past. + Version 8.2.0 - 6 March 2023 ---------------------------- - Various bugfixes & UI improvements. diff --git a/Makefile b/Makefile index 75abd7d29f..4c4eacf293 100644 --- a/Makefile +++ b/Makefile @@ -285,7 +285,7 @@ inplace-postinstall-nginx: inplace-postinstall-permissions # Removes created symlinks; generated logs, submissions, etc. remain in output subdir. inplace-uninstall-l: - rm -f $(judgehost_libjudgedir) + rm -rf $(judgehost_libjudgedir) rm -rf $(judgehost_bindir) # Rules to configure and build for a Coverity scan. diff --git a/README.md b/README.md index a0131d618b..c712d467f2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ DOMjudge [![Coverity Scan Status](https://img.shields.io/coverity/scan/671.svg)](https://scan.coverity.com/projects/domjudge) [![LGTM alerts](https://img.shields.io/lgtm/alerts/g/DOMjudge/domjudge.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/DOMjudge/domjudge/alerts/) -This is the Programming Contest Jury System "DOMjudge" version 8.2.0 +This is the Programming Contest Jury System "DOMjudge" version 8.2.3 DOMjudge is a system for running a programming contest, like the ICPC regional and world championship programming contests. @@ -44,7 +44,7 @@ https://github.com/DOMjudge/domjudge/wiki Copyright & Licensing --------------------- -DOMjudge is Copyright (c) 2004 - 2023 by the DOMjudge developers and +DOMjudge is Copyright (c) 2004 - 2024 by the DOMjudge developers and all respective contributors. The current DOMjudge developers are Jaap Eldering, Nicky Gerritsen, Keith Johnson, Thijs Kinkhorst, Mart Pluijmaekers, Michael Vasseur and Tobias Werth; see the manual for diff --git a/configure.ac b/configure.ac index 41d005b8a0..2a06c0f258 100644 --- a/configure.ac +++ b/configure.ac @@ -65,7 +65,7 @@ AC_ARG_WITH([domjudge-user], [AS_HELP_STRING([--with-domjudge-user=USER], [User that owns password files (default: current user).])], [], []) if test "x$with_domjudge_user" = x; then - user=m4_esyscmd([id -un]) + user="$(id -un)" # Check and warn if running as root (without explicitly setting it): if test "x$user" = xroot; then AC_MSG_ERROR([installing/running as root is STRONGLY DISCOURAGED, use --with-domjudge-user=root to override.]) @@ -117,6 +117,13 @@ else AC_MSG_RESULT($RUNUSER) fi +if test "x${DOMJUDGE_USER#"$RUNUSER"}" != "x$DOMJUDGE_USER" ; then + AC_MSG_ERROR([domjudge_user '$DOMJUDGE_USER' cannot match runuser '$RUNUSER'.]) +fi +if test "x$RUNUSER" = "xroot" ; then + AC_MSG_ERROR([runuser cannot be root.]) +fi + AC_MSG_CHECKING([rungroup]) AC_ARG_WITH([rungroup], [AS_HELP_STRING([--with-rungroup=GROUP], [Unprivileged group under which to run submissions (default: same as runuser).])], [], []) @@ -209,7 +216,7 @@ AX_WITH_COMMENT(7,[ ]) # {{{ Directory for systemd unit files -PKG_PROG_PKG_CONFIG +PKG_PROG_PKG_CONFIG() AC_ARG_WITH([systemdsystemunitdir], [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files])],, [with_systemdsystemunitdir=auto]) diff --git a/doc/manual/conf.py b/doc/manual/conf.py index 1838ce0e91..43785ce39e 100644 --- a/doc/manual/conf.py +++ b/doc/manual/conf.py @@ -148,8 +148,6 @@ \definecolor{noteBgColor}{rgb}{1,0,1} \definecolor{sphinxnoteBgColor}{RGB}{221,233,239} -\renewenvironment{sphinxnote}[1] -{\begin{sphinxheavybox}\sphinxstrong{#1} }{\end{sphinxheavybox}} \usepackage{fancyhdr} \pagestyle{fancy} diff --git a/doc/manual/config-advanced.rst b/doc/manual/config-advanced.rst index 52c9dcd270..72b8016740 100644 --- a/doc/manual/config-advanced.rst +++ b/doc/manual/config-advanced.rst @@ -7,7 +7,7 @@ DOMjudge can optionally present country flags, affiliation logos, team pictures and a page-wide banner on the public interface. You can place the images under the path `public/images/` (see -the Config checker in the admin interfae for the full filesystem +the Config checker in the admin interface for the full filesystem path of your installation) as follows: - *Country flags* are shown when the ``show_flags`` configuration option @@ -405,7 +405,7 @@ Clearing the PHP/Symfony cache ------------------------------ Some operations require you to clear the PHP/Symfony cache. To do this, execute -the `webapp/bin/console` (see the Config checker in the admin interfae for the +the `webapp/bin/console` (see the Config checker in the admin interface for the full filesystem path of your installation) binary with the `cache:clear` subcommand:: webapp/bin/console cache:clear diff --git a/doc/manual/import.rst b/doc/manual/import.rst index 8a25924f64..cbd543b560 100644 --- a/doc/manual/import.rst +++ b/doc/manual/import.rst @@ -10,7 +10,7 @@ in your `.netrc`_ file. You need to install `httpie`_ and replace the ```` in the examples below with the API URL of your local DOMjudge installation. -For using the CLI, you need to replace ```` with the path to +To use the CLI, you need to replace ```` with the path to the ``webapp`` directory of the DOMserver. Importing team categories @@ -402,11 +402,11 @@ and click `Import`. To import the file using the API run the following commands:: - http --check-status -b -f POST "/contests//problems" data@problems.yaml + http --check-status -b -f POST "/contests//problems/add-data" data@problems.yaml To import the file using the CLI run the following command:: - /bin/console api:call -m POST -f data=problems.yaml contests//problems + /bin/console api:call -m POST -f data=problems.yaml contests//problems/add-data Replace ```` with the contest ID that was returned when importing the contest metadata. @@ -444,11 +444,11 @@ Call it from your contest folder like this:: misc-tools/import-contest -to use the API, or:: +to use the API, or simply:: - misc-tools/import-contest + misc-tools/import-contest -to use the CLI. +to use the CLI. In this case you must run it from the DOMserver. Importing from ICPC CMS API --------------------------- @@ -487,11 +487,11 @@ called `config.json` in your current directory:: misc-tools/configure-domjudge -to use the API or:: +to use the API, or simply:: - misc-tools/configure-domjudge + misc-tools/configure-domjudge -to use the CLI. +to use the CLI. In this case you must run it from the DOMserver. .. _CCS specification: https://ccs-specs.icpc.io/2022-07/ccs_system_requirements#appendix-file-formats .. _.netrc: https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html diff --git a/doc/manual/install-domserver.rst b/doc/manual/install-domserver.rst index 9546ab92a3..7cc61c0f7e 100644 --- a/doc/manual/install-domserver.rst +++ b/doc/manual/install-domserver.rst @@ -35,14 +35,14 @@ For your convenience, the following command will install the necessary software on the DOMjudge server as mentioned above when using Debian GNU/Linux, or one of its derivative distributions like Ubuntu:: - sudo apt install acl zip unzip mariadb-server apache2 \ + sudo apt install libcgroup-dev make acl zip unzip mariadb-server apache2 \ php php-fpm php-gd php-cli php-intl php-mbstring php-mysql \ php-curl php-json php-xml php-zip composer ntp The following command can be used on RedHat Enterprise Linux, and related distributions like CentOS and Fedora:: - sudo yum install acl zip unzip mariadb-server httpd \ + sudo yum install make libcgroup-devel acl zip unzip mariadb-server httpd \ php-gd php-cli php-intl php-mbstring php-mysqlnd \ php-xml php-zip composer ntp @@ -57,9 +57,9 @@ taken than simply running ``./configure && make && make install``. After installing the required software as described above, run configure. In this example to install DOMjudge in the directory ``domjudge`` under -your home directory:: +`/opt`:: - ./configure --prefix=$HOME/domjudge + ./configure --prefix=/opt/domjudge make domserver sudo make install-domserver diff --git a/doc/manual/install-judgehost.rst b/doc/manual/install-judgehost.rst index 77f1b8b370..dbb23e2f41 100644 --- a/doc/manual/install-judgehost.rst +++ b/doc/manual/install-judgehost.rst @@ -54,10 +54,9 @@ These instructions assume a release `tarball for instructions to build from git sources. After installing the software listed above, run configure. In this -example to install DOMjudge in the directory ``domjudge`` under your -home directory:: +example to install DOMjudge in the directory ``domjudge`` under `/opt`:: - ./configure --prefix=$HOME/domjudge + ./configure --prefix=/opt/domjudge make judgehost sudo make install-judgehost @@ -165,7 +164,8 @@ any other tasks on the same CPU core the judgedaemon is using: On modern distros (e.g. Debian bullseye and Ubuntu Jammy Jellyfish) which have cgroup v2 enabled by default, you need to add ``systemd.unified_cgroup_hierarchy=0`` -as well. Then run ``update-grub`` and reboot. +as well. If you are running systemd v257, you also need to add `SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1`. +Then run ``update-grub`` and reboot. After rebooting check that ``/proc/cmdline`` actually contains the added kernel options. On VM hosting providers such as Google Cloud or DigitalOcean, ``GRUB_CMDLINE_LINUX_DEFAULT`` may be overwritten diff --git a/doc/manual/judging.rst b/doc/manual/judging.rst index 1cad85dda6..509d1d2f47 100644 --- a/doc/manual/judging.rst +++ b/doc/manual/judging.rst @@ -9,10 +9,12 @@ The flow of an incoming submission is as follows. checks, or accepted and stored as a *submission*. #. The first available *judgehost* compiles, runs and checks the submission. The outcome and outputs are stored as a - *judging* of this submission. Note that judgehosts may be - restricted to certain contests, languages and problems, so that it can be - the case that a judgehost is available, but not judging an available - submission. + *judging* of this submission. If parallel judging is enabled, + multiple judgehosts may pick up and work on the same submission + (if there is no queue of pending submissions). +#. To avoid starving other teams from judgehost resources, + submissions from teams that submit while they have other submissions + in the judge queue will get lower priority than a submission from a team that has no earlier submission being judged yet. #. If verification is not required, the result is automatically recorded and the team can view the result and the scoreboard is updated (unless after the scoreboard freeze). A judge can diff --git a/doc/manual/problem-format.rst b/doc/manual/problem-format.rst index ebbbf03355..341ddc853e 100644 --- a/doc/manual/problem-format.rst +++ b/doc/manual/problem-format.rst @@ -39,4 +39,4 @@ problem by uploading a zip file that contains only testcase files. Any jury solutions present will be automatically submitted when ``allow_submit`` is ``1`` and there's a team associated with the uploading user. -.. _ICPC problem package specification: https://icpc.io/problem-package-format/spec/problem_package_format +.. _ICPC problem package specification: https://icpc.io/problem-package-format/spec/legacy-icpc diff --git a/docker-compose.yml b/docker-compose.yml index bb93035b6b..5b65bf0847 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: image: domjudge/domjudge-contributor hostname: domjudge-contributor volumes: - - /sys/fs/cgroup:/sys/fs/cgroup:ro + - /sys/fs/cgroup:/sys/fs/cgroup - .:/domjudge:cached - /chroot links: diff --git a/example_problems/hello/submissions/compiler_error/objectcode/message.o b/example_problems/hello/submissions/compiler_error/objectcode/message.o new file mode 100644 index 0000000000..9f8a565eac Binary files /dev/null and b/example_problems/hello/submissions/compiler_error/objectcode/message.o differ diff --git a/example_problems/hello/submissions/accepted/test-hello-space.c b/example_problems/hello/submissions/multiple/test-hello-space.c similarity index 100% rename from example_problems/hello/submissions/accepted/test-hello-space.c rename to example_problems/hello/submissions/multiple/test-hello-space.c diff --git a/gitlab/ci/template.yml b/gitlab/ci/template.yml index 242395d1c4..43d88bc5da 100644 --- a/gitlab/ci/template.yml +++ b/gitlab/ci/template.yml @@ -27,7 +27,7 @@ .cached_vendor: extends: [.clean_ordering] cache: - key: libvendor-260522 + key: libvendor-82 paths: - lib/vendor/ @@ -35,7 +35,7 @@ script: - /bin/true services: - - name: mysql + - name: mysql:8.0 command: ["--default-authentication-plugin=mysql_native_password"] alias: sqlserver diff --git a/gitlab/ci_settings.sh b/gitlab/ci_settings.sh index ad1eef947a..dfea8a1925 100755 --- a/gitlab/ci_settings.sh +++ b/gitlab/ci_settings.sh @@ -13,6 +13,8 @@ export GITSHA export PS4='(${BASH_SOURCE}:${LINENO}): - [$?] $ ' export LOGFILE="/opt/domjudge/domserver/webapp/var/log/prod.log" +CCS_SPECS_PINNED_SHA1='a68aff54c4e60fc2bff2fc5c36c119bffa4d30f1' + # Shared storage for all artifacts export GITLABARTIFACTS="$DIR/gitlabartifacts" mkdir -p "$GITLABARTIFACTS" diff --git a/gitlab/integration.sh b/gitlab/integration.sh index 1b470f0e50..5a75f90819 100755 --- a/gitlab/integration.sh +++ b/gitlab/integration.sh @@ -81,11 +81,25 @@ mount mount -o remount,exec,dev /builds section_end mount +section_start check_cgroup_v1 "Checking for cgroup v1 availability" +grep cgroup$ /proc/filesystems +if [ $? -eq 0 ]; then + cgroupv1=1 +else + echo "Skipping tests that rely on cgroup v1" + cgroupv1=0 +fi +section_end check_cgroup_v1 + section_start judgehost "Configure judgehost" cd /opt/domjudge/judgehost/ sudo cp /opt/domjudge/judgehost/etc/sudoers-domjudge /etc/sudoers.d/ sudo chmod 400 /etc/sudoers.d/sudoers-domjudge -sudo bin/create_cgroups +if [ $cgroupv1 -ne 0 ]; then + # We allow this to go wrong as some gitlab runners do not have the + # swapaccount kernel option set. + sudo bin/create_cgroups || cgroupv1=0 +fi if [ ! -d ${DIR}/chroot/domjudge/ ]; then cd ${DIR}/misc-tools @@ -94,13 +108,15 @@ fi section_end judgehost section_start more_setup "Remaining setup (e.g. starting judgedaemon)" -# download domjudge-scripts for API check + +# Download yajsv and ccs-specs for API check. cd $HOME -composer -n require justinrainbow/json-schema +curl -o yajsv https://github.com/neilpa/yajsv/releases/download/v1.4.1/yajsv.linux.amd64 +chmod a+x yajsv echo -e "\033[0m" -PATH=${PATH}:${HOME}/vendor/bin -git clone --depth=1 https://github.com/DOMjudge/domjudge-scripts.git -CHECK_API=${HOME}/domjudge-scripts/contest-api/check-api.sh +git clone https://github.com/icpc/ccs-specs.git +( cd ccs-specs && git reset --hard $CCS_SPECS_PINNED_SHA1 ) +CHECK_API="${HOME}/ccs-specs/check-api.sh -j ${HOME}/yajsv" # Recreate domjudge-run-0 user with random UID to prevent clashes with # existing users in the host and other CI jobs, which can lead to @@ -128,8 +144,10 @@ set -e if [ $PIN_JUDGEDAEMON -eq 1 ]; then PINNING="-n 0" fi -sudo -u domjudge bin/judgedaemon $PINNING |& tee /tmp/judgedaemon.log & -sleep 5 +if [ $cgroupv1 -ne 0 ]; then + sudo -u domjudge bin/judgedaemon $PINNING |& tee /tmp/judgedaemon.log & + sleep 5 +fi section_end more_setup @@ -154,9 +172,7 @@ for i in hello_kattis different guess; do done section_end submitting -section_start judging "Waiting until all submissions are judged" -# wait for and check results -NUMSUBS=$(curl --fail http://admin:$ADMINPASS@localhost/domjudge/api/contests/1/submissions | python3 -mjson.tool | grep -c '"id":') +section_start curlcookie "Preparing cookie jar for curl" export COOKIEJAR COOKIEJAR=$(mktemp --tmpdir) export CURLOPTS="--fail -sq -m 30 -b $COOKIEJAR" @@ -170,56 +186,64 @@ curl $CURLOPTS -c $COOKIEJAR -F "_csrf_token=$CSRFTOKEN" -F "_username=admin" -F curl $CURLOPTS -F "sendto=" -F "problem=1-" -F "bodytext=Testing" -F "submit=Send" \ "http://localhost/domjudge/jury/clarifications/send" -o /dev/null -# Don't spam the log. -set +x +section_end curlcookie -while /bin/true; do - sleep 30s - curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" -o /dev/null +if [ $cgroupv1 -ne 0 ]; then + section_start judging "Waiting until all submissions are judged" + # wait for and check results + NUMSUBS=$(curl --fail http://admin:$ADMINPASS@localhost/domjudge/api/contests/1/submissions | python3 -mjson.tool | grep -c '"id":') - # Check if we are done, i.e. everything is judged or something got disabled by internal error... - if tail /tmp/judgedaemon.log | grep -q "No submissions in queue"; then - break - fi - # ... or something has crashed. - if ! pgrep -f judgedaemon; then - break - fi -done + # Don't spam the log. + set +x -NUMNOTVERIFIED=$(curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions checked" | sed -r 's/^.* ([0-9]+) submissions checked.*$/\1/') -NUMVERIFIED=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions not checked" | sed -r 's/^.* ([0-9]+) submissions not checked.*$/\1/') -NUMNOMAGIC=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "without magic string" | sed -r 's/^.* ([0-9]+) without magic string.*$/\1/') -section_end judging - -# We expect -# - two submissions with ambiguous outcome, -# - no submissions without magic string, -# - and all submissions to be judged. -if [ $NUMNOTVERIFIED -ne 2 ] || [ $NUMNOMAGIC -ne 0 ] || [ $NUMSUBS -gt $((NUMVERIFIED+NUMNOTVERIFIED)) ]; then - section_start error "Short error description" - # We error out below anyway, so no need to fail earlier than that. - set +e - echo "verified subs: $NUMVERIFIED, unverified subs: $NUMNOTVERIFIED, total subs: $NUMSUBS" - echo "(expected 2 submissions to be unverified, but all to be processed)" - echo "Of these $NUMNOMAGIC do not have the EXPECTED_RESULTS string (should be 0)." - curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" | w3m -dump -T text/html - section_end error - - section_start logfiles "All the more or less useful logfiles" - for i in /opt/domjudge/judgehost/judgings/*/*/*/*/*/compile.out; do - echo $i; - head -n 100 $i; - dir=$(dirname $i) - if [ -r $dir/testcase001/system.out ]; then - head $dir/testcase001/system.out - head $dir/testcase001/runguard.err - head $dir/testcase001/program.err - head $dir/testcase001/program.meta + while /bin/true; do + sleep 30s + curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" -o /dev/null + + # Check if we are done, i.e. everything is judged or something got disabled by internal error... + if tail /tmp/judgedaemon.log | grep -q "No submissions in queue"; then + break + fi + # ... or something has crashed. + if ! pgrep -f judgedaemon; then + break fi - echo; done - exit 1; + + NUMNOTVERIFIED=$(curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions checked" | sed -r 's/^.* ([0-9]+) submissions checked.*$/\1/') + NUMVERIFIED=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions not checked" | sed -r 's/^.* ([0-9]+) submissions not checked.*$/\1/') + NUMNOMAGIC=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "without magic string" | sed -r 's/^.* ([0-9]+) without magic string.*$/\1/') + section_end judging + + # We expect + # - two submissions with ambiguous outcome, + # - no submissions without magic string, + # - and all submissions to be judged. + if [ $NUMNOTVERIFIED -ne 2 ] || [ $NUMNOMAGIC -ne 0 ] || [ $NUMSUBS -gt $((NUMVERIFIED+NUMNOTVERIFIED)) ]; then + section_start error "Short error description" + # We error out below anyway, so no need to fail earlier than that. + set +e + echo "verified subs: $NUMVERIFIED, unverified subs: $NUMNOTVERIFIED, total subs: $NUMSUBS" + echo "(expected 2 submissions to be unverified, but all to be processed)" + echo "Of these $NUMNOMAGIC do not have the EXPECTED_RESULTS string (should be 0)." + curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" | w3m -dump -T text/html + section_end error + + section_start logfiles "All the more or less useful logfiles" + for i in /opt/domjudge/judgehost/judgings/*/*/*/*/*/compile.out; do + echo $i; + head -n 100 $i; + dir=$(dirname $i) + if [ -r $dir/testcase001/system.out ]; then + head $dir/testcase001/system.out + head $dir/testcase001/runguard.err + head $dir/testcase001/program.err + head $dir/testcase001/program.meta + fi + echo; + done + exit 1; + fi fi section_start api_check "Performing API checks" @@ -239,7 +263,13 @@ if cat /opt/domjudge/domserver/webapp/var/log/prod.log | egrep '(CRITICAL|ERROR) fi # Check the Contest API: -$CHECK_API -n -C -e -a 'strict=1' http://admin:$ADMINPASS@localhost/domjudge/api +if [ $cgroupv1 -ne 0 ]; then + $CHECK_API -n -C -e -a 'strict=1' http://admin:$ADMINPASS@localhost/domjudge/api +else + # With cgroup v1 not being available we don't judge, so we cannot do + # consistency checks, so running the above command without -C. + $CHECK_API -n -e -a 'strict=1' http://admin:$ADMINPASS@localhost/domjudge/api +fi section_end api_check |& tee "$GITLABARTIFACTS/check_api.log" section_start validate_feed "Validate the eventfeed against API (ignoring failures)" diff --git a/judge/build_executable.sh b/judge/build_executable.sh index bc10ac2903..d5f77684c3 100755 --- a/judge/build_executable.sh +++ b/judge/build_executable.sh @@ -13,14 +13,14 @@ trap 'cleanup ; error' EXIT cleanup () { - $DJ_LIBJUDGEDIR/chroot-startstop.sh stop + "${DJ_LIBJUDGEDIR}/chroot-startstop.sh" stop # Make sure that all files are owned by the current user/group, so # that we can delete the judging output tree without root access. # We also remove group RUNGROUP so that this can safely be shared # across multiple judgedaemons, and remove write permissions. - $GAINROOT chown -R "$(id -un):" $WORKDIR - chmod -R go-w $WORKDIR + $GAINROOT chown -R "$(id -un):" "$WORKDIR" + chmod -R go-w "$WORKDIR" } cleanexit () @@ -66,10 +66,9 @@ if [ ! -d "$WORKDIR" ] || [ ! -w "$WORKDIR" ] || [ ! -x "$WORKDIR" ]; then fi [ -x "$RUNGUARD" ] || error "runguard not found or not executable: $RUNGUARD" -OLDDIR="$PWD" cd "$CHROOTDIR" -$DJ_LIBJUDGEDIR/chroot-startstop.sh start +"${DJ_LIBJUDGEDIR}/chroot-startstop.sh" start chmod a+rwx "$WORKDIR" diff --git a/judge/compile.sh b/judge/compile.sh index 2c91417a7e..7b2b005318 100755 --- a/judge/compile.sh +++ b/judge/compile.sh @@ -124,7 +124,7 @@ touch compile.out compile.meta # Copy compile script into chroot # shellcheck disable=SC2174 mkdir -m 0777 -p "$WORKDIR/compile-script" -cp -a $(dirname $COMPILE_SCRIPT)/* $PWD/compile-script +cp -a "$(dirname "$COMPILE_SCRIPT")"/* "$WORKDIR/compile-script/" cd "$WORKDIR/compile" @@ -152,7 +152,7 @@ $GAINROOT "$RUNGUARD" ${DEBUG:+-v} $CPUSET_OPT -u "$RUNUSER" -g "$RUNGROUP" \ -r "$PWD/.." -d "/compile" \ -m $SCRIPTMEMLIMIT -t $SCRIPTTIMELIMIT -c -f $SCRIPTFILELIMIT -s $SCRIPTFILELIMIT \ -M "$WORKDIR/compile.meta" $ENVIRONMENT_VARS -- \ - "/compile-script/$(basename $COMPILE_SCRIPT)" program "$MEMLIMIT" "$@" >"$WORKDIR/compile.tmp" 2>&1 || \ + "/compile-script/$(basename "$COMPILE_SCRIPT")" program "$MEMLIMIT" "$@" >"$WORKDIR/compile.tmp" 2>&1 || \ exitcode=$? # Make sure that all files are owned by the current user/group, so diff --git a/judge/create_cgroups.in b/judge/create_cgroups.in index 9be667510c..1f9471d284 100755 --- a/judge/create_cgroups.in +++ b/judge/create_cgroups.in @@ -9,32 +9,31 @@ JUDGEHOSTUSER=@DOMJUDGE_USER@ CGROUPBASE=@judgehost_cgroupdir@ +print_cgroup_instruction () { + echo "" + echo "To fix this, please make the following changes: + 1. In /etc/default/grub, add 'cgroup_enable=memory swapaccount=1' to GRUB_CMDLINE_LINUX_DEFAULT. + On modern distros (e.g. Debian bullseye and Ubuntu Jammy Jellyfish) which have cgroup v2 enabled by default, + you need to add 'systemd.unified_cgroup_hierarchy=0' as well. + 2. Run update-grub + 3. Reboot" >&2 + exit 1 +} + for i in cpuset memory; do mkdir -p $CGROUPBASE/$i if [ ! -d $CGROUPBASE/$i/ ]; then if ! mount -t cgroup -o$i $i $CGROUPBASE/$i/; then - echo "Error: Can not mount $i cgroup. Probably cgroup support is missing from running kernel. Unable to continue. - - To fix this, please make the following changes: - 1. In /etc/default/grub, add 'cgroup_enable=memory swapaccount=1' to GRUB_CMDLINE_LINUX_DEFAULT - 2. Run update-grub - 3. Reboot" >&2 - exit 1 + echo "Error: Can not mount $i cgroup. Probably cgroup support is missing from running kernel. Unable to continue." + print_cgroup_instruction fi fi mkdir -p $CGROUPBASE/$i/domjudge done if [ ! -f $CGROUPBASE/memory/memory.limit_in_bytes ] || [ ! -f $CGROUPBASE/memory/memory.memsw.limit_in_bytes ]; then - echo "Error: cgroup support missing memory features in running kernel. Unable to continue. - - To fix this, please make the following changes: - 1. In /etc/default/grub, add 'cgroup_enable=memory swapaccount=1' to GRUB_CMDLINE_LINUX_DEFAULT. - On modern distros (e.g. Debian bullseye and Ubuntu Jammy Jellyfish) which have cgroup v2 enabled by default, - you need to add 'systemd.unified_cgroup_hierarchy=0' as well. - 2. Run update-grub - 3. Reboot" >&2 - exit 1 + echo "Error: cgroup support missing memory features in running kernel. Unable to continue." + print_cgroup_instruction fi chown -R $JUDGEHOSTUSER $CGROUPBASE/*/domjudge diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index 35db3c2389..3f0a50f92b 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -1053,7 +1053,7 @@ function compile( ): bool { global $myhost, $EXITCODES; - // Re-use compilation if it already exists. + // Reuse compilation if it already exists. if (file_exists("$workdir/compile.success")) { return true; } diff --git a/judge/runguard.c b/judge/runguard.c index 4304aca48d..a5257d67db 100644 --- a/judge/runguard.c +++ b/judge/runguard.c @@ -780,7 +780,7 @@ void setrestrictions() setlim(STACK); if ( filesize!=RLIM_INFINITY ) { - verbose("setting filesize limit to %d bytes",(int)filesize); + verbose("setting filesize limit to %lu bytes",filesize); lim.rlim_cur = lim.rlim_max = filesize; setlim(FSIZE); } @@ -1198,7 +1198,9 @@ int main(int argc, char **argv) cgroup_create(); - unshare(CLONE_FILES|CLONE_FS|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWNS|CLONE_NEWUTS|CLONE_SYSVSEM); + if ( unshare(CLONE_FILES|CLONE_FS|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWNS|CLONE_NEWUTS|CLONE_SYSVSEM)!=0 ) { + error(errno, "calling unshare"); + } /* Check if any Linux Out-Of-Memory killer adjustments have to * be made. The oom_adj or oom_score_adj is inherited by child @@ -1242,6 +1244,13 @@ int main(int argc, char **argv) } verbose("pipes closed in child"); + if ( outputmeta ) { + if ( fclose(metafile)!=0 ) { + error(errno,"closing file `%s'",metafilename); + } + verbose("metafile closed in child"); + } + /* And execute child command. */ execvp(cmdname,cmdargs); error(errno,"cannot start `%s'",cmdname); diff --git a/misc-tools/configure-domjudge.in b/misc-tools/configure-domjudge.in index fa13db6d6b..cc2078acd1 100755 --- a/misc-tools/configure-domjudge.in +++ b/misc-tools/configure-domjudge.in @@ -3,7 +3,7 @@ ''' configure-domjudge -- Convenience script to update DOMjudge configuration. -Reads credentials from ~/.netrc. +Reads credentials from ~/.netrc when using the API. See also https://www.domjudge.org/docs/manual/main/import.html (replace main with the DOMjudge major.minor version if you are running a @@ -23,8 +23,10 @@ from typing import List, Set sys.path.append('@domserver_libdir@') import dj_utils +webappdir = '@domserver_webappdir@' + def usage(): - print(f'Usage {sys.argv[0]} ') + print(f'Usage: {sys.argv[0]} []') exit(1) @@ -40,7 +42,7 @@ def compare_configs(expected_config: Set, actual_config: Set, num_spaces=4) -> ( diffs.extend(d) else: diffs.append(f'{space_string}- {k}:\n {space_string}is: {actual_config[k]}\n {space_string}new: {expected_config[k]}') - + new_keys = set(expected_config.keys()).difference(set(actual_config.keys())) missing_keys = set(actual_config.keys()).difference(set(expected_config.keys())) return (diffs, new_keys, missing_keys) @@ -50,11 +52,13 @@ def _keyify_list(l: List) -> Set: return { elem['id']: elem for elem in l } -if len(sys.argv) < 2: +if len(sys.argv) == 1: + dj_utils.domjudge_webapp_folder_or_api_url = webappdir +elif len(sys.argv) == 2: + dj_utils.domjudge_webapp_folder_or_api_url = sys.argv[1] +else: usage() -dj_utils.domjudge_webapp_folder_or_api_url = sys.argv[1] - user_data = dj_utils.do_api_request('user') if 'admin' not in user_data['roles']: print('Your user does not have the \'admin\' role, can not import.') @@ -79,9 +83,7 @@ if os.path.exists('config.json'): print(f' - missing keys from new config = {missing_keys}') if diffs or new_keys or missing_keys: if dj_utils.confirm(' - Upload these configuration changes?', True): - url = f'{dj_utils.api_url}/config' - response = requests.put(url, headers=dj_utils.headers, json=expected_config) - actual_config = dj_utils.parse_api_response('config', response) + actual_config = dj_utils.do_api_request('config', 'PUT', expected_config) diffs, new_keys, missing_keys = compare_configs( actual_config=actual_config, expected_config=expected_config diff --git a/misc-tools/dj_judgehost_cleanup.in b/misc-tools/dj_judgehost_cleanup.in index 06e5b61477..39ceebc7e6 100755 --- a/misc-tools/dj_judgehost_cleanup.in +++ b/misc-tools/dj_judgehost_cleanup.in @@ -40,7 +40,7 @@ EOF kill_processes() { echo "Killing remaining processes..." - pkill -G @RUNGROUP@ + pkill -G @RUNGROUP@ || true echo "Done." } @@ -76,7 +76,7 @@ cleanup_judgings() { if cut -d ' ' -f 2 /proc/mounts | grep -E "^$JUDGINGDIR/" >/dev/null 2>&1; then echo "There are (stale) bind mounts under $JUDGINGDIR/." echo "Make sure that all judgedaemons on this host are stopped, and then" - echo "run '$PROGNAME mount' to clean up any stale bind mounts." + echo "run '$PROGNAME mounts' to clean up any stale bind mounts." exit 1 fi echo "Cleaning up judging data..." diff --git a/misc-tools/dj_make_chroot.in b/misc-tools/dj_make_chroot.in index a3006441b4..1e2ecefb86 100755 --- a/misc-tools/dj_make_chroot.in +++ b/misc-tools/dj_make_chroot.in @@ -67,7 +67,7 @@ Options: -i List of extra package names to install (comma separated). -r List of extra package names to remove (comma separated). -l List of local package files to install (comma separated). - -s List of apt repository .list files that exist outside the chroot + -s List of apt repository .list files that exist outside the chroot to add to the chroot (comma separated). Signing keys for the repository will be imported if they exist as .gpg, .asc or .arm. @@ -178,7 +178,7 @@ if [ "$DISTRO" = 'Debian' ]; then REMOVEDEBS="" # Which debootstrap package to install on non-Debian systems: - DEBOOTDEB="debootstrap_1.0.114_all.deb" + DEBOOTDEB="debootstrap_1.0.128+nmu2+deb12u2_all.deb" # The Debian mirror/proxy below can be passed as environment # variables; if none are given the following defaults are used. diff --git a/misc-tools/dj_utils.py b/misc-tools/dj_utils.py index 75e2d9c98b..0d63cbd714 100644 --- a/misc-tools/dj_utils.py +++ b/misc-tools/dj_utils.py @@ -44,10 +44,16 @@ def parse_api_response(name: str, response: requests.Response): return None # We got a successful HTTP response. It worked. Return the full response - return json.loads(response.text) + try: + result = json.loads(response.text) + except json.decoder.JSONDecodeError as e: + print(response.text) + raise RuntimeError(f'Failed to JSON decode the response for API request {name}') + + return result -def do_api_request(name: str): +def do_api_request(name: str, method: str = 'GET', jsonData: dict = {}): '''Perform an API call to the given endpoint and return its data. Based on whether `domjudge_webapp_folder_or_api_url` is a folder or URL this @@ -55,6 +61,8 @@ def do_api_request(name: str): Parameters: name (str): the endpoint to call + method (str): the method to use, GET or PUT are supported + jsonData (dict): the JSON data to PUT. Only used when method is PUT Returns: The endpoint contents. @@ -64,13 +72,16 @@ def do_api_request(name: str): ''' if os.path.isdir(domjudge_webapp_folder_or_api_url): - return api_via_cli(name) + return api_via_cli(name, method, {}, {}, jsonData) else: global ca_check url = f'{domjudge_webapp_folder_or_api_url}/{name}' try: - response = requests.get(url, headers=headers, verify=ca_check) + if method == 'GET': + response = requests.get(url, headers=headers, verify=ca_check) + elif method == 'PUT': + response = requests.put(url, headers=headers, verify=ca_check, json=jsonData) except requests.exceptions.SSLError as e: ca_check = not confirm( "Can not verify certificate, ignore certificate check?", False) @@ -83,7 +94,6 @@ def do_api_request(name: str): raise RuntimeError(e) return parse_api_response(name, response) - def upload_file(name: str, apifilename: str, file: str, data: dict = {}): '''Upload the given file to the API at the given path with the given name. @@ -126,14 +136,15 @@ def upload_file(name: str, apifilename: str, file: str, data: dict = {}): return parse_api_response(name, response) -def api_via_cli(name: str, method: str = 'GET', data: dict = {}, files: dict = {}): +def api_via_cli(name: str, method: str = 'GET', data: dict = {}, files: dict = {}, jsonData: dict = {}): '''Perform the given API request using the CLI Parameters: name (str): the endpoint to call - method (str): the method to use. Either GET or POST - data (dict): the POST data to use. Only used when method is POST - files (dict): the files to use. Only used when method is POST + method (str): the method to use. Either GET, POST or PUT + data (dict): the POST data to use. Only used when method is POST or PUT + files (dict): the files to use. Only used when method is POST or PUT + jsonData (dict): the JSON data to use. Only used when method is POST or PUT Returns: The parsed endpoint contents. @@ -155,6 +166,9 @@ def api_via_cli(name: str, method: str = 'GET', data: dict = {}, files: dict = { for item in files: command.extend(['-f', f'{item}={files[item]}']) + if jsonData: + command.extend(['-j', json.dumps(jsonData)]) + command.append(name) result = subprocess.run(command, capture_output=True) diff --git a/misc-tools/import-contest.in b/misc-tools/import-contest.in index dcc4aa3e0a..13e07ff61a 100755 --- a/misc-tools/import-contest.in +++ b/misc-tools/import-contest.in @@ -2,8 +2,8 @@ ''' import-contest -- Convenience script to import a contest (including metadata, -teams and problems) via the command line to the DOMjudge API or to a DOMjudge -installation using the CLI. +teams and problems) from the command line. Defaults to using the CLI interface; +Specify a DOMjudge API URL as to use that. Reads credentials from ~/.netrc when using the API. @@ -27,10 +27,11 @@ sys.path.append('@domserver_libdir@') import dj_utils cid = None +webappdir = '@domserver_webappdir@' def usage(): - print(f'Usage {sys.argv[0]} ') + print(f'Usage: {sys.argv[0]} []') exit(1) @@ -92,12 +93,13 @@ def import_images(entity: str, property: str, filename_regexes: List[str]): else: print(f'Skipping {entity} {property} import.') - -if len(sys.argv) < 2: +if len(sys.argv) == 1: + dj_utils.domjudge_webapp_folder_or_api_url = webappdir +elif len(sys.argv) == 2: + dj_utils.domjudge_webapp_folder_or_api_url = sys.argv[1] +else: usage() -dj_utils.domjudge_webapp_folder_or_api_url = sys.argv[1] - user_data = dj_utils.do_api_request('user') if 'admin' not in user_data['roles']: print('Your user does not have the \'admin\' role, can not import.') diff --git a/sql/dj_setup_database.in b/sql/dj_setup_database.in index 5d6c11e606..b04f62e7d0 100755 --- a/sql/dj_setup_database.in +++ b/sql/dj_setup_database.in @@ -38,6 +38,7 @@ Commands: Options: -u connect to MySQL with DB admin -p use password for DB admin user + -q be (mostly) quiet -r read DB admin password from prompt -s connect via local socket (do not specify port) @@ -50,6 +51,8 @@ EOF # Wrapper around mysql command to allow setting options, user, etc. mysql() { + local user pass + # shellcheck disable=SC2153 if [ -n "$DBUSER" ]; then user="-u $DBUSER" @@ -224,6 +227,13 @@ remove_db_users() verbose "DOMjudge database and user(s) removed." } +install_examples() +{ + DBUSER=$domjudge_DBUSER PASSWD=$domjudge_PASSWD symfony_console domjudge:load-example-data + "$EXAMPLEPROBDIR"/generate-contest-yaml + ( cd "$EXAMPLEPROBDIR" && yes y | "$BINDIR"/import-contest ) +} + ### Script starts here ### # Parse command-line options: @@ -286,10 +296,7 @@ uninstall) install-examples) read_dbpasswords - - DBUSER=$domjudge_DBUSER PASSWD=$domjudge_PASSWD symfony_console domjudge:load-example-data - "$EXAMPLEPROBDIR"/generate-contest-yaml - ( cd "$EXAMPLEPROBDIR" && yes y | "$BINDIR"/import-contest "${WEBAPPDIR}" ) + install_examples ;; install-loadtest) @@ -316,11 +323,7 @@ bare-install|install) unset DB_FIRST_INSTALL DBUSER=$domjudge_DBUSER PASSWD=$domjudge_PASSWD symfony_console domjudge:load-default-data if [ "$1" = "install" ]; then - DBUSER=$domjudge_DBUSER PASSWD=$domjudge_PASSWD symfony_console domjudge:load-example-data - "$EXAMPLEPROBDIR"/generate-contest-yaml - ( cd "$EXAMPLEPROBDIR" && yes y | "$BINDIR"/import-contest "${WEBAPPDIR}" ) - fi - if [ "$1" = "install" ]; then + install_examples verbose "SQL structure and default/example data installed." else verbose "SQL structure and defaults installed (no sample data)." diff --git a/sql/files/defaultdata/plg/compile.plg b/sql/files/defaultdata/plg/compile.plg index 368a5f6781..cec2b596be 100644 --- a/sql/files/defaultdata/plg/compile.plg +++ b/sql/files/defaultdata/plg/compile.plg @@ -4,7 +4,7 @@ :- dynamic(loading/1). :- asserta(loading(0)). -% The first hook is for detectin +% The first hook is for detecting % loading state user:message_hook(Term, _, _) :- diff --git a/webapp/migrations/Version20191031203138.php b/webapp/migrations/Version20191031203138.php index c2a3790c5b..00cebcd6d3 100644 --- a/webapp/migrations/Version20191031203138.php +++ b/webapp/migrations/Version20191031203138.php @@ -29,17 +29,14 @@ public function getDescription(): string public function up(Schema $schema): void { - $em = $this->container->get('doctrine')->getManager(); - /** @var Language $python2 */ - $python2 = $em->getRepository(Language::class)->find('py2'); - /** @var Language $python3 */ - $python3 = $em->getRepository(Language::class)->find('py3'); - if ($python2 === null || - $python3 === null || - $python2->getAllowSubmit() || - $python3->getAllowSubmit() || - $python2->getExtensions() !== ['py2', 'py'] || - $python3->getExtensions() !== ['py3']) { + $python2 = $this->connection->fetchAssociative('SELECT * FROM language WHERE langid = :py2', ['py2' => 'py2']); + $python3 = $this->connection->fetchAssociative('SELECT * FROM language WHERE langid = :py3', ['py3' => 'py3']); + if ($python2 === false || + $python3 === false || + $python2['allow_submit'] || + $python3['allow_submit'] || + json_decode($python2['extensions'], true) !== ['py2', 'py'] || + json_decode($python3['extensions'], true) !== ['py3']) { return; } @@ -49,19 +46,15 @@ public function up(Schema $schema): void public function down(Schema $schema): void { - // this down() migration is auto-generated, please modify it to your needs - $em = $this->container->get('doctrine')->getManager(); - /** @var Language $python2 */ - $python2 = $em->getRepository(Language::class)->find('py2'); - /** @var Language $python3 */ - $python3 = $em->getRepository(Language::class)->find('py3'); + $python2 = $this->connection->fetchAssociative('SELECT * FROM language WHERE langid = :py2', ['py2' => 'py2']); + $python3 = $this->connection->fetchAssociative('SELECT * FROM language WHERE langid = :py3', ['py3' => 'py3']); - if ($python2 === null || - $python3 === null || - $python2->getAllowSubmit() || - $python3->getAllowSubmit() || - $python2->getExtensions() !== ['py2', 'py'] || - $python3->getExtensions() !== ['py3']) { + if ($python2 === false || + $python3 === false || + $python2['allow_submit'] || + $python3['allow_submit'] || + json_decode($python2['extensions'], true) !== ['py2'] || + json_decode($python3['extensions'], true) !== ['py3', 'py']) { return; } diff --git a/webapp/migrations/Version20200131064449.php b/webapp/migrations/Version20200131064449.php index de7c3e7f8d..47b18d3be4 100644 --- a/webapp/migrations/Version20200131064449.php +++ b/webapp/migrations/Version20200131064449.php @@ -30,10 +30,9 @@ public function up(Schema $schema) : void $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); // Note: can't use ConfigurationService::get on 'registration_category_name' because the specification has been removed from db-config.yaml - $em = $this->container->get('doctrine')->getManager(); - $registrationCategoryNameConfig = $em->getRepository(Configuration::class)->findOneBy(['name' => 'registration_category_name']); + $registrationCategoryNameConfig = $this->connection->fetchAssociative('SELECT * FROM configuration WHERE name = :registration_category_name', ['registration_category_name' => 'registration_category_name']); if ($registrationCategoryNameConfig) { - $registrationCategoryName = $registrationCategoryNameConfig->getValue(); + $registrationCategoryName = $registrationCategoryNameConfig['value']; } else { $registrationCategoryName = ''; } @@ -53,23 +52,19 @@ public function down(Schema $schema) : void { $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); - $em = $this->container->get('doctrine')->getManager(); - $selfRegistrationCategories = $em->getRepository(TeamCategory::class)->findBy( - ['allow_self_registration' => 1], - ['sortorder' => 'ASC'] - ); + $selfRegistrationCategories = $this->connection->fetchAllAssociative('SELECT * FROM team_category WHERE allow_self_registration = 1 ORDER BY sortorder'); $this->warnIf( count($selfRegistrationCategories) > 1, sprintf('Team categories for self-registered teams were %s. Only first will be kept.', implode(', ', array_map(function($category) { - return $category->getName(); + return $category['name']; }, $selfRegistrationCategories))) ); $this->addSql( "INSERT INTO configuration (name, value) VALUES ('registration_category_name', :value)", - ['value' => empty($selfRegistrationCategories) ? '""' : json_encode($selfRegistrationCategories[0]->getName())] + ['value' => empty($selfRegistrationCategories) ? '""' : json_encode($selfRegistrationCategories[0]['name'])] ); $this->addSql('ALTER TABLE team_category DROP allow_self_registration'); diff --git a/webapp/migrations/Version20230508153514.php b/webapp/migrations/Version20230508153514.php new file mode 100644 index 0000000000..34e0edee79 --- /dev/null +++ b/webapp/migrations/Version20230508153514.php @@ -0,0 +1,38 @@ +addSql('DROP INDEX externalid ON problem'); + $this->addSql('CREATE UNIQUE INDEX externalid ON problem (externalid(190))'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX externalid ON problem'); + $this->addSql('CREATE INDEX externalid ON problem (externalid(190))'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20230508163415.php b/webapp/migrations/Version20230508163415.php new file mode 100644 index 0000000000..9cef0b66ba --- /dev/null +++ b/webapp/migrations/Version20230508163415.php @@ -0,0 +1,51 @@ +connection->fetchAllAssociative('SELECT immutable_execid FROM immutable_executable'); + foreach ($immutableExecutables as $immutableExecutable) { + $files = $this->connection->fetchAllAssociative('SELECT hash, filename, is_executable FROM executable_file WHERE immutable_execid = :id', ['id' => $immutableExecutable['immutable_execid']]); + uasort($files, fn(array $a, array $b) => strcmp($a['filename'], $b['filename'])); + $newHash = md5( + join( + array_map( + fn(array $file) => $file['hash'] . $file['filename'] . (bool)$file['is_executable'], + $files + ) + ) + ); + $this->connection->executeQuery('UPDATE immutable_executable SET hash = :hash WHERE immutable_execid = :id', [ + 'hash' => $newHash, + 'id' => $immutableExecutable['immutable_execid'], + ]); + } + } + + public function down(Schema $schema): void + { + // We don't handle this case + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Command/CallApiActionCommand.php b/webapp/src/Command/CallApiActionCommand.php index db666494f2..61712bf884 100644 --- a/webapp/src/Command/CallApiActionCommand.php +++ b/webapp/src/Command/CallApiActionCommand.php @@ -47,13 +47,19 @@ protected function configure(): void 'data', 'd', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'POST data to use as key=value. Only allowed when the method is POST' + 'POST data to use as key=value. Only allowed when the method is POST or PUT' + ) + ->addOption( + 'json', + 'j', + InputOption::VALUE_REQUIRED, + 'JSON body data to use. Only allowed when the method is POST or PUT' ) ->addOption( 'file', 'f', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Files to use as field=filename. Only allowed when the method is POST' + 'Files to use as field=filename. Only allowed when the method is POST or PUT' ) ->addOption( 'user', @@ -63,10 +69,10 @@ protected function configure(): void ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - if (!in_array($input->getOption('method'), [Request::METHOD_GET, Request::METHOD_POST], true)) { - $output->writeln('Error: only GET and POST methods are supported'); + if (!in_array($input->getOption('method'), [Request::METHOD_GET, Request::METHOD_POST, Request::METHOD_PUT], true)) { + $output->writeln('Error: only GET, POST and PUT methods are supported'); return self::FAILURE; } @@ -98,7 +104,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $data = []; $files = []; - if ($input->getOption('method') === Request::METHOD_POST) { + if (in_array($input->getOption('method'), [Request::METHOD_POST, Request::METHOD_PUT], true)) { foreach ($input->getOption('data') as $dataItem) { $parts = explode('=', $dataItem, 2); if (count($parts) !== 2) { @@ -109,6 +115,10 @@ protected function execute(InputInterface $input, OutputInterface $output) $data[$parts[0]] = $parts[1]; } + if ($json = $input->getOption('json')) { + $data = array_merge($data, $this->dj->jsonDecode($json)); + } + foreach ($input->getOption('file') as $fileItem) { $parts = explode('=', $fileItem, 2); if (count($parts) !== 2) { diff --git a/webapp/src/Controller/API/ContestController.php b/webapp/src/Controller/API/ContestController.php index 2f12401f91..e77056ae41 100644 --- a/webapp/src/Controller/API/ContestController.php +++ b/webapp/src/Controller/API/ContestController.php @@ -763,7 +763,7 @@ public function samplesDataZipAction(Request $request): Response ->getOneOrNullResult(); if ($contest === null) { - throw new NotFoundHttpException(sprintf('Object with ID \'%s\' not found', $id)); + throw new NotFoundHttpException(sprintf('Object with ID \'%s\' not found', $request->attributes->get('cid'))); } return $this->dj->getSamplesZipForContest($contest); diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index a2303a6d90..a9c4a5e0a2 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -820,7 +820,9 @@ public function internalErrorAction(Request $request): ?int $this->em->persist($error); // Even if there are no remaining judge tasks for this judging open (which is covered by the transaction below), // we need to mark this judging as internal error. - $judging->setInternalError($error); + if ($judging) { + $judging->setInternalError($error); + } $this->em->flush(); if ($field_name !== null) { diff --git a/webapp/src/Controller/API/JudgementController.php b/webapp/src/Controller/API/JudgementController.php index 55bf7e9ab5..462bd7a135 100644 --- a/webapp/src/Controller/API/JudgementController.php +++ b/webapp/src/Controller/API/JudgementController.php @@ -44,6 +44,7 @@ public function __construct( $verdictsConfig = $this->dj->getDomjudgeEtcDir() . '/verdicts.php'; $this->verdicts = include $verdictsConfig; + $this->verdicts['aborted'] = 'JE'; /* happens for aborted judgings */ } /** @@ -149,9 +150,9 @@ protected function getQueryBuilder(Request $request): QueryBuilder $specificJudgingRequested = $request->attributes->has('id') || $request->query->has('ids'); - // If we don't have correct permissions or didn't request a specific - // judging (necessary for the event log), then exclude some judgings: - if (!$roleAllowsVisibility && !$specificJudgingRequested) { + // Only include invalid or too late submissions if the role allows it + // and we request these specific submissions. + if (!($roleAllowsVisibility && $specificJudgingRequested)) { $queryBuilder ->andWhere('s.submittime < c.endtime') ->andWhere('j.valid = 1'); diff --git a/webapp/src/Controller/API/ScoreboardController.php b/webapp/src/Controller/API/ScoreboardController.php index b97a3b1cb6..e96562390f 100644 --- a/webapp/src/Controller/API/ScoreboardController.php +++ b/webapp/src/Controller/API/ScoreboardController.php @@ -125,7 +125,6 @@ public function getScoreboardAction(Request $request): array $contest = $this->em->getRepository(Contest::class)->find($this->getContestId($request)); // Get the event for this scoreboard. - // TODO: Add support for after_event_id. /** @var Event $event */ $event = $this->em->createQueryBuilder() ->from(Event::class, 'e') @@ -141,12 +140,14 @@ public function getScoreboardAction(Request $request): array if ($event) { // Build up scoreboard results. $results = [ - 'event_id' => (string)$event->getEventid(), 'time' => Utils::absTime($event->getEventtime()), 'contest_time' => Utils::relTime($event->getEventtime() - $contest->getStarttime()), 'state' => $contest->getState(), 'rows' => [], ]; + if (!$request->query->getBoolean('strict')) { + $results['event_id'] = (string)$event->getEventid(); + } } // Return early if there's nothing to display yet. diff --git a/webapp/src/Controller/Jury/AuditLogController.php b/webapp/src/Controller/Jury/AuditLogController.php index 0b0c65e2a1..b38100e5c2 100644 --- a/webapp/src/Controller/Jury/AuditLogController.php +++ b/webapp/src/Controller/Jury/AuditLogController.php @@ -168,7 +168,7 @@ private function generateDatatypeUrl(string $type, $id): ?string return $this->generateUrl('jury_user', ['userId' => $id]); case 'testcase': $testcase = $this->em->getRepository(Testcase::class)->find($id); - if ($testcase) { + if ($testcase && $testcase->getProblem()) { return $this->generateUrl('jury_problem_testcases', ['probId' => $testcase->getProblem()->getProbid()]); } break; diff --git a/webapp/src/Controller/Jury/ClarificationController.php b/webapp/src/Controller/Jury/ClarificationController.php index 3ae7f8c620..b4734dc2fd 100644 --- a/webapp/src/Controller/Jury/ClarificationController.php +++ b/webapp/src/Controller/Jury/ClarificationController.php @@ -202,7 +202,7 @@ public function viewAction(int $id): Response $data['answered'] = $clar->getAnswered(); - $data['body'] = Utils::wrapUnquoted($clar->getBody(), 78); + $data['body'] = $clar->getBody(); $clardata['list'][] = $data; } diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index 12ef2dab07..987d05e18e 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -379,7 +379,6 @@ public function indexAction(Request $request): Response 'upcoming_contest' => $upcomingContest, 'contests_table' => $contests_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 2 : 0, ]); } diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index 676073187f..9cbb7afec1 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -116,7 +116,6 @@ public function indexAction(Request $request): Response return $this->render('jury/executables.html.twig', [ 'executables' => $executables_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 3 : 0, 'form' => $form->createView(), ]); } @@ -241,6 +240,18 @@ public function viewAction(Request $request, string $execId): Response $this->em->persist($executableFile); $files[] = $executableFile; } + $offset = count($files); + foreach ($editorData['skippedBinary'] as $idx => $skippedBinaryData) { + $origExecutableFile = $this->em->getRepository(ExecutableFile::class)->find($skippedBinaryData['execfileid']); + $executableFile = new ExecutableFile(); + $executableFile + ->setRank($idx + $offset) + ->setIsExecutable($origExecutableFile->isExecutable()) + ->setFilename($origExecutableFile->getFilename()) + ->setFileContent($origExecutableFile->getFileContent()); + $this->em->persist($executableFile); + $files[] = $executableFile; + } $immutableExecutable = new ImmutableExecutable($files); $this->em->persist($immutableExecutable); @@ -436,7 +447,10 @@ protected function dataForEditor(Executable $executable): array $content = $file->getFileContent(); $rank = $file->getRank(); if (!mb_detect_encoding($content, null, true)) { - $skippedBinary[] = $filename; + $skippedBinary[] = [ + 'filename' => $filename, + 'execfileid' => $file->getExecFileId(), + ]; continue; // Skip binary files. } $filenames[] = $filename; diff --git a/webapp/src/Controller/Jury/ImportExportController.php b/webapp/src/Controller/Jury/ImportExportController.php index 8d5629b01c..2092b1d8c8 100644 --- a/webapp/src/Controller/Jury/ImportExportController.php +++ b/webapp/src/Controller/Jury/ImportExportController.php @@ -184,22 +184,17 @@ public function indexAction(Request $request): Response $this->dj->auditlog('problem', $newProblem->getProbid(), 'upload zip', $clientName); } else { - $this->addFlash('danger', implode("\n", $allMessages)); + $this->postMessages($allMessages); return $this->redirectToRoute('jury_problems'); } } catch (Exception $e) { - $allMessages[] = $e->getMessage(); + $allMessages['danger'][] = $e->getMessage(); } finally { if (isset($zip)) { $zip->close(); } } - - foreach (['info', 'warning', 'danger'] as $type) { - if (!empty($allMessages[$type])) { - $this->addFlash($type, implode("\n", $allMessages[$type])); - } - } + $this->postMessages($allMessages); if ($newProblem !== null) { return $this->redirectToRoute('jury_problem', ['probId' => $newProblem->getProbid()]); @@ -575,4 +570,16 @@ protected function getClarificationsHtml(): Response 'problems' => $contestProblems, ]); } + + /** + * @param array $allMessages + */ + private function postMessages(array $allMessages): void + { + foreach (['info', 'warning', 'danger'] as $type) { + if (!empty($allMessages[$type])) { + $this->addFlash($type, implode("\n", $allMessages[$type])); + } + } + } } diff --git a/webapp/src/Controller/Jury/InternalErrorController.php b/webapp/src/Controller/Jury/InternalErrorController.php index 973fd6eed2..99b46c978c 100644 --- a/webapp/src/Controller/Jury/InternalErrorController.php +++ b/webapp/src/Controller/Jury/InternalErrorController.php @@ -117,8 +117,12 @@ public function viewAction(int $errorId): Response case 'judgehost': // Judgehosts get disabled by their hostname, so we need to look it up here. $judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $disabled['hostname']]); - $affectedLink = $this->generateUrl('jury_judgehost', ['judgehostid' => $judgehost->getJudgehostid()]); $affectedText = $disabled['hostname']; + if ($judgehost) { + $affectedLink = $this->generateUrl('jury_judgehost', ['judgehostid' => $judgehost->getJudgehostid()]); + } else { + $affectedText .= ' (deleted)'; + } break; case 'language': $affectedLink = $this->generateUrl('jury_language', ['langId' => $disabled['langid']]); @@ -153,10 +157,10 @@ public function handleAction(Request $request, ?Profiler $profiler, int $errorId /** @var InternalError $internalError */ $internalError = $this->em->createQueryBuilder() ->from(InternalError::class, 'e') - ->join('e.affectedJudgings', 'j') - ->join('j.submission', 's') - ->join('j.contest', 'c') - ->join('s.team', 't') + ->leftJoin('e.affectedJudgings', 'j') + ->leftJoin('j.submission', 's') + ->leftJoin('j.contest', 'c') + ->leftJoin('s.team', 't') ->leftJoin('s.rejudging', 'r') ->select('e, j, s, c, t, r') ->where('e.errorid = :id') @@ -195,7 +199,7 @@ public function handleAction(Request $request, ?Profiler $profiler, int $errorId sprintf('internal error: %s', InternalErrorStatusType::STATUS_RESOLVED)); $affectedJudgings = $internalError->getAffectedJudgings(); - if (!empty($affectedJudgings)) { + if (!$affectedJudgings->isEmpty()) { $skipped = []; $rejudging = $this->rejudgingService->createRejudging( 'Internal Error ' . $internalError->getErrorid() . ' resolved', @@ -220,6 +224,8 @@ public function handleAction(Request $request, ?Profiler $profiler, int $errorId ); $progressReporter(100, '', $message); } + } else { + $progressReporter(100, '', 'No affected judgings.'); } }); }); diff --git a/webapp/src/Controller/Jury/JudgehostController.php b/webapp/src/Controller/Jury/JudgehostController.php index d4b0cdc807..afd7a024bc 100644 --- a/webapp/src/Controller/Jury/JudgehostController.php +++ b/webapp/src/Controller/Jury/JudgehostController.php @@ -184,7 +184,6 @@ public function indexAction(Request $request): Response $data = [ 'judgehosts' => $judgehosts_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 2 : 0, 'all_checked_in_recently' => $all_checked_in_recently, 'refresh' => [ 'after' => 5, diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index f5f6c1683c..fb3c5e0020 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -146,7 +146,6 @@ public function indexAction(): Response 'enabled_languages' => $enabled_languages, 'disabled_languages' => $disabled_languages, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 2 : 0, ]); } diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index fb345628ec..cfd509f905 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -158,7 +158,15 @@ public function indexAction(): Response 'probId' => $p->getProbid(), ]) ]; - $problemactions[] = [ + + $problemIsLocked = false; + foreach ($p->getContestProblems() as $contestProblem) { + if ($contestProblem->getContest()->isLocked()) { + $problemIsLocked = true; + } + } + + $deleteAction = [ 'icon' => 'trash-alt', 'title' => 'delete this problem', 'link' => $this->generateUrl('jury_problem_delete', [ @@ -166,6 +174,12 @@ public function indexAction(): Response ]), 'ajaxModal' => true, ]; + if ($problemIsLocked) { + $deleteAction['title'] .= ' - problem belongs to a locked contest'; + $deleteAction['disabled'] = true; + unset($deleteAction['link']); + } + $problemactions[] = $deleteAction; } // Add formatted {mem,output}limit row data for the table. @@ -200,7 +214,6 @@ public function indexAction(): Response $data = [ 'problems' => $problems_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 4 : 2, ]; return $this->render('jury/problems.html.twig', $data); @@ -248,7 +261,10 @@ public function exportAction(int $problemId): StreamedResponse $yaml = ['name' => $problem->getName()]; if (!empty($problem->getCompareExecutable())) { $yaml['validation'] = 'custom'; + } elseif ($problem->getCombinedRunCompare() && !empty($problem->getRunExecutable())) { + $yaml['validation'] = 'custom interactive'; } + if (!empty($problem->getSpecialCompareArgs())) { $yaml['validator_flags'] = $problem->getSpecialCompareArgs(); } @@ -278,6 +294,27 @@ public function exportAction(int $problemId): StreamedResponse stream_get_contents($problem->getProblemtext())); } + $compareExecutable = null; + if ($problem->getCompareExecutable()) { + $compareExecutable = $problem->getCompareExecutable(); + } elseif ($problem->getCombinedRunCompare()) { + $compareExecutable = $problem->getRunExecutable(); + } + if ($compareExecutable) { + foreach ($compareExecutable->getImmutableExecutable()->getFiles() as $file) { + $filename = sprintf('output_validators/%s/%s', $compareExecutable->getExecid(), $file->getFilename()); + $zip->addFromString($filename, $file->getFileContent()); + if ($file->isExecutable()) { + // 100755 = regular file, executable + $zip->setExternalAttributesName( + $filename, + ZipArchive::OPSYS_UNIX, + octdec('100755') << 16 + ); + } + } + } + foreach ([true, false] as $isSample) { /** @var Testcase[] $testcases */ $testcases = $this->em->createQueryBuilder() @@ -935,12 +972,7 @@ public function editAction(Request $request, int $probId): Response $zip->close(); } } - - foreach (['info', 'warning', 'danger'] as $type) { - if (!empty($messages[$type])) { - $this->addFlash($type, implode("\n", $messages[$type])); - } - } + $this->postMessages($messages); return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); } @@ -1135,4 +1167,16 @@ public function requestRemainingRunsWholeProblemAction(string $probId): Redirect $this->judgeRemaining($judgings); return $this->redirect($this->generateUrl('jury_problem', ['probId' => $probId])); } + + /** + * @param array $allMessages + */ + private function postMessages(array $allMessages): void + { + foreach (['info', 'warning', 'danger'] as $type) { + if (!empty($allMessages[$type])) { + $this->addFlash($type, implode("\n", $allMessages[$type])); + } + } + } } diff --git a/webapp/src/Controller/Jury/TeamAffiliationController.php b/webapp/src/Controller/Jury/TeamAffiliationController.php index 62c12c5750..02436c76d5 100644 --- a/webapp/src/Controller/Jury/TeamAffiliationController.php +++ b/webapp/src/Controller/Jury/TeamAffiliationController.php @@ -143,7 +143,6 @@ public function indexAction(string $projectDir): Response return $this->render('jury/team_affiliations.html.twig', [ 'team_affiliations' => $team_affiliations_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 2 : 0, ]); } diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 9c0eecd9e6..789c12db82 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -127,7 +127,6 @@ public function indexAction(): Response return $this->render('jury/team_categories.html.twig', [ 'team_categories' => $team_categories_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 2 : 0, ]); } diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index f4056c460d..500badb58e 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -239,7 +239,6 @@ public function indexAction(): Response return $this->render('jury/teams.html.twig', [ 'teams' => $teams_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 3 : 1, ]); } diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 7aed63c520..f69387ea69 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -184,7 +184,6 @@ public function indexAction(): Response return $this->render('jury/users.html.twig', [ 'users' => $users_table, 'table_fields' => $table_fields, - 'num_actions' => $this->isGranted('ROLE_ADMIN') ? 2 : 0, ]); } diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index 073c2506c6..83e15f62b1 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -104,10 +104,12 @@ public function homeAction(Request $request): Response $clarifications = $this->em->createQueryBuilder() ->from(Clarification::class, 'c') ->leftJoin('c.problem', 'p') + ->leftJoin('p.contest_problems', 'cp') ->leftJoin('c.sender', 's') ->leftJoin('c.recipient', 'r') - ->select('c', 'p') + ->select('c', 'cp', 'p') ->andWhere('c.contest = :contest') + ->andWhere('cp.contest = :contest') ->andWhere('c.sender IS NULL') ->andWhere('c.recipient = :team OR c.recipient IS NULL') ->setParameter('contest', $contest) @@ -121,10 +123,12 @@ public function homeAction(Request $request): Response $clarificationRequests = $this->em->createQueryBuilder() ->from(Clarification::class, 'c') ->leftJoin('c.problem', 'p') + ->leftJoin('p.contest_problems', 'cp') ->leftJoin('c.sender', 's') ->leftJoin('c.recipient', 'r') - ->select('c', 'p') + ->select('c', 'cp', 'p') ->andWhere('c.contest = :contest') + ->andWhere('cp.contest = :contest') ->andWhere('c.sender = :team') ->setParameter('contest', $contest) ->setParameter('team', $team) diff --git a/webapp/src/Entity/Problem.php b/webapp/src/Entity/Problem.php index d3364e3e2b..15fcc9edac 100644 --- a/webapp/src/Entity/Problem.php +++ b/webapp/src/Entity/Problem.php @@ -20,6 +20,12 @@ * @ORM\Entity() * @ORM\Table( * name="problem", + * uniqueConstraints={ + * @ORM\UniqueConstraint( + * name="externalid", + * columns={"externalid"} + * ) + * }, * options={"collation"="utf8mb4_unicode_ci", "charset"="utf8mb4","comment"="Problems the teams can submit solutions for"}, * indexes={ * @ORM\Index(name="externalid", columns={"externalid"}, options={"lengths": {190}}), diff --git a/webapp/src/Form/Type/ContestProblemType.php b/webapp/src/Form/Type/ContestProblemType.php index 18bdee292f..b4068abf3b 100644 --- a/webapp/src/Form/Type/ContestProblemType.php +++ b/webapp/src/Form/Type/ContestProblemType.php @@ -30,14 +30,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->add('shortname', TextType::class, [ 'label' => 'Short name', ]); - $builder->add('points', IntegerType::class); + $builder->add('points', IntegerType::class,[ + 'label' => 'Points', + ]); $builder->add('allowSubmit', ChoiceType::class, [ + 'label' => 'Allow submit', 'choices' => [ 'Yes' => true, 'No' => false, ], ]); $builder->add('allowJudge', ChoiceType::class, [ + 'label' => 'Allow judge', 'choices' => [ 'Yes' => true, 'No' => false, diff --git a/webapp/src/Form/Type/JudgehostType.php b/webapp/src/Form/Type/JudgehostType.php index 5986f8682f..ebaa87d233 100644 --- a/webapp/src/Form/Type/JudgehostType.php +++ b/webapp/src/Form/Type/JudgehostType.php @@ -14,15 +14,18 @@ class JudgehostType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('hostname', TextType::class, [ + 'label' => 'Hostname', 'attr' => ['readonly' => true], ]); $builder->add('enabled', ChoiceType::class, [ + 'label' => 'Enabled', 'choices' => [ 'yes' => true, 'no' => false, ], ]); $builder->add('hidden', ChoiceType::class, [ + 'label' => 'Hidden', 'choices' => [ 'yes' => true, 'no' => false, diff --git a/webapp/src/Service/AwardService.php b/webapp/src/Service/AwardService.php index 6863109b84..57281ff4f8 100644 --- a/webapp/src/Service/AwardService.php +++ b/webapp/src/Service/AwardService.php @@ -60,8 +60,19 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void $additionalBronzeMedals = $contest->getB() ?? 0; - // Can we assume this is ordered just walk the first 12+B entries? + $currentSortOrder = -1; + + // For every team that we skip because it is not in a medal category, we need to include one + // additional rank. So keep track of the number of skipped teams + $skippedTeams = 0; + foreach ($scoreboard->getScores() as $teamScore) { + // If we are checking a new sort order, reset the number of skipped teams + if ($teamScore->team->getCategory()->getSortorder() !== $currentSortOrder) { + $currentSortOrder = $teamScore->team->getCategory()->getSortorder(); + $skippedTeams = 0; + } + if ($teamScore->numPoints == 0) { continue; } @@ -70,13 +81,17 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void if ($rank === 1) { $overall_winners[] = $teamid; } - if ($contest->getMedalsEnabled() && $contest->getMedalCategories()->contains($teamScore->team->getCategory())) { - if ($rank <= $contest->getGoldMedals()) { - $medal_winners['gold'][] = $teamid; - } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals()) { - $medal_winners['silver'][] = $teamid; - } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $additionalBronzeMedals) { - $medal_winners['bronze'][] = $teamid; + if ($contest->getMedalsEnabled()) { + if ($contest->getMedalCategories()->contains($teamScore->team->getCategory())) { + if ($rank - $skippedTeams <= $contest->getGoldMedals()) { + $medal_winners['gold'][] = $teamid; + } elseif ($rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals()) { + $medal_winners['silver'][] = $teamid; + } elseif ($rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $additionalBronzeMedals) { + $medal_winners['bronze'][] = $teamid; + } + } else { + $skippedTeams++; } } } diff --git a/webapp/src/Service/ImportProblemService.php b/webapp/src/Service/ImportProblemService.php index 0569d5204a..2a4d380721 100644 --- a/webapp/src/Service/ImportProblemService.php +++ b/webapp/src/Service/ImportProblemService.php @@ -430,6 +430,13 @@ public function importZippedProblem( } } + if (str_contains($testInput, "\r")) { + $messages['warning'][] = "Testcase file '$baseFileName.in' contains Windows newlines."; + } + if (str_contains($testOutput, "\r")) { + $messages['warning'][] = "Testcase file '$baseFileName.ans' contains Windows newlines."; + } + $md5in = md5($testInput); $md5out = md5($testOutput); @@ -818,6 +825,20 @@ public function importZippedProblem( $messages['info'][] = sprintf('Saved problem %d', $problem->getProbid()); + // Only here disable problem submit to make sure the jury submissions + // do get added above. + if ($contestProblem) { + $this->em->flush(); + $testcases = $problem->getTestcases()->toArray(); + if (count(array_filter($testcases, function($t) { return !$t->getDeleted(); }))==0) { + $messages['danger'][] = 'No testcases present, disabling submitting for this problem'; + $contestProblem->setAllowSubmit(false); + } + } + + // Make sure we persisted all changes to DB + $this->em->flush(); + return $problem; } diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 8447ab5fb4..4e1470b1b9 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -128,6 +128,7 @@ public function getFilters(): array new TwigFilter('printWarningContent', [$this, 'printWarningContent'], ['is_safe' => ['html']]), new TwigFilter('entityIdBadge', [$this, 'entityIdBadge'], ['is_safe' => ['html']]), new TwigFilter('medalType', [$this->awards, 'medalType']), + new TwigFilter('numTableActions', [$this, 'numTableActions']), ]; } @@ -776,7 +777,7 @@ public function interactiveLog(string $log, bool $forTeam = false): string if (empty($content)) { break; } - $content = htmlspecialchars($content); + $content = Utils::specialchars($content); $content = '' . str_replace("\n", "\u{21B5}
", $content) . ''; @@ -1207,4 +1208,13 @@ public function entityIdBadge(BaseApiEntity $entity, string $idPrefix = ''): str 'externalId' => $externalIdField ? $propertyAccessor->getValue($entity, $externalIdField) : null, ]); } + + public function numTableActions(array $tableData): int + { + $maxNumActions = 0; + foreach ($tableData as $item) { + $maxNumActions = max($maxNumActions, count($item['actions'] ?? [])); + } + return $maxNumActions; + } } diff --git a/webapp/src/Utils/Utils.php b/webapp/src/Utils/Utils.php index e525914d31..914d962bfd 100644 --- a/webapp/src/Utils/Utils.php +++ b/webapp/src/Utils/Utils.php @@ -335,7 +335,7 @@ public static function parseHexColor(string $hex): array } /** - * Comvert an RGB component to its hex value. + * Convert an RGB component to its hex value. */ public static function componentToHex(int $component): string { diff --git a/webapp/templates/jury/analysis/contest_overview.html.twig b/webapp/templates/jury/analysis/contest_overview.html.twig index 05ce5a47c0..fbe6eeaf5d 100644 --- a/webapp/templates/jury/analysis/contest_overview.html.twig +++ b/webapp/templates/jury/analysis/contest_overview.html.twig @@ -205,10 +205,10 @@ $(function() { {% set id=j.submitid %} {{ id }} - {{ j.judgingid }} - {{ j.submittime | printtime }} - {{ j.num_judgings }} - {{ j.timediff | number_format(2) }}s + {{ j.judgingid }} + {{ j.submittime | printtime }} + {{ j.num_judgings }} + {{ j.timediff | number_format(2) }}s {% endfor %} diff --git a/webapp/templates/jury/check_judgings.html.twig b/webapp/templates/jury/check_judgings.html.twig index 69c140692e..c8e0b7c807 100644 --- a/webapp/templates/jury/check_judgings.html.twig +++ b/webapp/templates/jury/check_judgings.html.twig @@ -46,7 +46,7 @@ {% elseif id == 'multiple' %} is judged as {{ result.actual }} but has multiple possible outcomes ({{ result.expected|join(', ') }}) {% elseif id == 'verified' %} - verfied as '{{ result.actual }}' + verified as '{{ result.actual }}' {% elseif id == 'nomatch' %} expected results unknown, leaving submission unchecked {% elseif id == 'earlier' %} diff --git a/webapp/templates/jury/clarification.html.twig b/webapp/templates/jury/clarification.html.twig index 5ab9ece9a6..9d8d8c83a2 100644 --- a/webapp/templates/jury/clarification.html.twig +++ b/webapp/templates/jury/clarification.html.twig @@ -110,7 +110,7 @@ Jury -
{{ clar.body }}
+
{{ clar.body|wrapUnquoted(78) }}
diff --git a/webapp/templates/jury/clarifications.html.twig b/webapp/templates/jury/clarifications.html.twig index d06d57ef57..c8c1eded26 100644 --- a/webapp/templates/jury/clarifications.html.twig +++ b/webapp/templates/jury/clarifications.html.twig @@ -81,7 +81,7 @@ {%- endif %} {% endif %} - {% if currentFilter is null or currentFilter == 'old' %} + {% if currentFilter is null or currentFilter == 'handled' %}

Handled requests

{%- if oldClarifications | length == 0 %}

No old clarification requests.

diff --git a/webapp/templates/jury/contests.html.twig b/webapp/templates/jury/contests.html.twig index a9af41e1a4..04cc260fc8 100644 --- a/webapp/templates/jury/contests.html.twig +++ b/webapp/templates/jury/contests.html.twig @@ -87,7 +87,7 @@

All available contests

- {{ macros.table(contests_table, table_fields, num_actions) }} + {{ macros.table(contests_table, table_fields) }} {% if is_granted('ROLE_ADMIN') %}

diff --git a/webapp/templates/jury/executable.html.twig b/webapp/templates/jury/executable.html.twig index a5f041b8a9..15a07a295f 100644 --- a/webapp/templates/jury/executable.html.twig +++ b/webapp/templates/jury/executable.html.twig @@ -82,8 +82,8 @@

We exclude these files from editing since we could not detect their encoding (e.g. they are binary files):
    - {% for file in skippedBinary %} -
  • {{ file }}
  • + {% for data in skippedBinary %} +
  • {{ data.filename }}
  • {% endfor %}
diff --git a/webapp/templates/jury/executables.html.twig b/webapp/templates/jury/executables.html.twig index 94af4cae5b..cd5276371e 100644 --- a/webapp/templates/jury/executables.html.twig +++ b/webapp/templates/jury/executables.html.twig @@ -12,7 +12,7 @@

Executables

- {{ macros.table(executables, table_fields, num_actions, {'ordering': 'false'}) }} + {{ macros.table(executables, table_fields, {'ordering': 'false'}) }} {% if is_granted('ROLE_ADMIN') %}

diff --git a/webapp/templates/jury/internal_error.html.twig b/webapp/templates/jury/internal_error.html.twig index 3ba03ea3d8..fb809430ac 100644 --- a/webapp/templates/jury/internal_error.html.twig +++ b/webapp/templates/jury/internal_error.html.twig @@ -72,7 +72,13 @@ {% if affectedText is not null %} Affected {{ internalError.disabled.kind }} - {{ affectedText }} + + {% if affectedLink %} + {{ affectedText }} + {% else %} + {{ affectedText }} + {% endif %} + {% endif %} diff --git a/webapp/templates/jury/jury_macros.twig b/webapp/templates/jury/jury_macros.twig index f86c3e08ac..ddb70eb30a 100644 --- a/webapp/templates/jury/jury_macros.twig +++ b/webapp/templates/jury/jury_macros.twig @@ -85,12 +85,13 @@ {% endmacro %} -{% macro table(data, fields, num_actions, options) %} +{% macro table(data, fields, options) %}

+ {%- set num_actions = data | numTableActions %} {%- set default_sort = 0 %} {%- set default_sort_order = 'asc' %} {%- for key,column in fields %} diff --git a/webapp/templates/jury/languages.html.twig b/webapp/templates/jury/languages.html.twig index 3a68e483b7..0df29e4c15 100644 --- a/webapp/templates/jury/languages.html.twig +++ b/webapp/templates/jury/languages.html.twig @@ -12,14 +12,14 @@

Enabled languages

- {{ macros.table(enabled_languages, table_fields, num_actions) }} + {{ macros.table(enabled_languages, table_fields) }}

Disabled languages

- {{ macros.table(disabled_languages, table_fields, num_actions) }} + {{ macros.table(disabled_languages, table_fields) }} {% if is_granted('ROLE_ADMIN') %}

diff --git a/webapp/templates/jury/partials/judgehost_list.html.twig b/webapp/templates/jury/partials/judgehost_list.html.twig index aa31d1bb6d..a317734554 100644 --- a/webapp/templates/jury/partials/judgehost_list.html.twig +++ b/webapp/templates/jury/partials/judgehost_list.html.twig @@ -1,2 +1,2 @@ {% import "jury/jury_macros.twig" as macros %} -{{ macros.table(judgehosts, table_fields, num_actions, {ordering: 'false'}) }} +{{ macros.table(judgehosts, table_fields, {ordering: 'false'}) }} diff --git a/webapp/templates/jury/problems.html.twig b/webapp/templates/jury/problems.html.twig index 727b515ab2..511fcc5cbb 100644 --- a/webapp/templates/jury/problems.html.twig +++ b/webapp/templates/jury/problems.html.twig @@ -12,7 +12,7 @@

Problems

- {{ macros.table(problems, table_fields, num_actions) }} + {{ macros.table(problems, table_fields) }} {% if is_granted('ROLE_ADMIN') %}

diff --git a/webapp/templates/jury/team_affiliations.html.twig b/webapp/templates/jury/team_affiliations.html.twig index 4c5503003a..0241c263b0 100644 --- a/webapp/templates/jury/team_affiliations.html.twig +++ b/webapp/templates/jury/team_affiliations.html.twig @@ -12,7 +12,7 @@

Affiliations

- {{ macros.table(team_affiliations, table_fields, num_actions) }} + {{ macros.table(team_affiliations, table_fields) }} {%- if is_granted('ROLE_ADMIN') %} diff --git a/webapp/templates/jury/team_categories.html.twig b/webapp/templates/jury/team_categories.html.twig index 9024a8bc79..91e7a95b96 100644 --- a/webapp/templates/jury/team_categories.html.twig +++ b/webapp/templates/jury/team_categories.html.twig @@ -12,7 +12,7 @@

Categories

- {{ macros.table(team_categories, table_fields, num_actions) }} + {{ macros.table(team_categories, table_fields) }} {% if is_granted('ROLE_ADMIN') %}

diff --git a/webapp/templates/jury/teams.html.twig b/webapp/templates/jury/teams.html.twig index 4d1eeb11be..b2501cf750 100644 --- a/webapp/templates/jury/teams.html.twig +++ b/webapp/templates/jury/teams.html.twig @@ -12,7 +12,7 @@

Teams

- {{ macros.table(teams, table_fields, num_actions) }} + {{ macros.table(teams, table_fields) }} {%- if is_granted('ROLE_ADMIN') %} diff --git a/webapp/templates/jury/users.html.twig b/webapp/templates/jury/users.html.twig index e629db218f..e5e291843d 100644 --- a/webapp/templates/jury/users.html.twig +++ b/webapp/templates/jury/users.html.twig @@ -12,7 +12,7 @@

Users

- {{ macros.table(users, table_fields, num_actions) }} + {{ macros.table(users, table_fields) }} {% if is_granted('ROLE_ADMIN') %}

diff --git a/webapp/tests/Unit/Service/AwardServiceTest.php b/webapp/tests/Unit/Service/AwardServiceTest.php index dbf833d303..7baf6982aa 100644 --- a/webapp/tests/Unit/Service/AwardServiceTest.php +++ b/webapp/tests/Unit/Service/AwardServiceTest.php @@ -21,10 +21,10 @@ class AwardServiceTest extends KernelTestCase protected function setUp(): void { - // The contest will have 1 gold, 1 silver and 2 bronze medals + // The contest will have 2 gold, 2 silver and 2 bronze medals, awarded only to category A and C $this->contest = (new Contest()) ->setMedalsEnabled(true) - ->setGoldMedals(1) + ->setGoldMedals(2) ->setSilverMedals(1) ->setBronzeMedals(1); $categoryA = (new TeamCategory()) @@ -33,22 +33,33 @@ protected function setUp(): void $categoryB = (new TeamCategory()) ->setName('Category B') ->setExternalid('cat_B'); + $categoryC = (new TeamCategory()) + ->setName('Category C') + ->setExternalid('cat_C'); $this->contest ->addMedalCategory($categoryA) - ->addMedalCategory($categoryB); + ->addMedalCategory($categoryC); $reflectedProblem = new ReflectionClass(TeamCategory::class); - $teamIdProperty = $reflectedProblem->getProperty('categoryid'); - $teamIdProperty->setAccessible(true); - $teamIdProperty->setValue($categoryA, 1); - $teamIdProperty->setValue($categoryB, 2); + $categoryIdProperty = $reflectedProblem->getProperty('categoryid'); + $categoryIdProperty->setAccessible(true); + $categoryIdProperty->setValue($categoryA, 1); + $categoryIdProperty->setValue($categoryB, 2); + $categoryIdProperty->setValue($categoryC, 3); $categories = [$categoryA, $categoryB]; - // Create 4 teams, each belonging to a category + // Create 9 teams, each belonging to a different category $teams = []; - foreach (['A', 'B', 'C', 'D'] as $teamLetter) { + foreach (['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] as $teamLetter) { + $category = $categoryC; + if (in_array($teamLetter, ['A', 'B', 'C'])) { + $category = $categoryA; + } + if (in_array($teamLetter, ['D', 'E', 'F'])) { + $category = $categoryB; + } $team = (new Team()) ->setName('Team ' . $teamLetter) ->setExternalid('team_' . $teamLetter) - ->setCategory(in_array($teamLetter, ['A', 'B']) ? $categoryA : $categoryB) + ->setCategory($category) ->setAffiliation(); // No affiliation needed $reflectedProblem = new ReflectionClass(Team::class); $teamIdProperty = $reflectedProblem->getProperty('teamid'); @@ -81,16 +92,25 @@ protected function setUp(): void // -----+----------- // A | 1 5 10 20 // B | x 2 3 x - // C | x x x 4 - // D | x x x x + // C | x 2 3 x + // D | x x x 12 + // E | x x x 13 + // F | x x x 14 + // G | x x x 15 + // H | x x x x + // I | x x x x // - // THis means A is the overall winner, will get a gold medal and is the winner + // This means A is the overall winner, will get a gold medal and is the winner // of category A. It is also first to solve problem A. - // B is second, so it gets a silver medal. It is also first to solve problem B and C - // C is the first to solve problem D, gets a bronze medal and is winner of category B. - // D didn't solve anything, so it will not get any medals at all + // B is second, so it also gets a gold medal. It is also first to solve problem B and C + // C scored the exact same as B, so it also gets the same medals + // D is the first to solve problem D and is the winner of category B. But will not get any medal. + // E and F will get no awards at all. + // G is the winner of category C and will get a bronze medal. + // The reason G doesn't get silver is that C would get silver if it was ranked differently, + // but it is not. + // H and I didn't solve anything, so it will not get any medals at all - $minute = 60; // Indexed first by team, then by problem $scores = [ 'A' => [ @@ -104,7 +124,20 @@ protected function setUp(): void 'C' => 3, ], 'C' => [ - 'D' => 4, + 'B' => 2, + 'C' => 3, + ], + 'D' => [ + 'D' => 12, + ], + 'E' => [ + 'D' => 13, + ], + 'F' => [ + 'D' => 14, + ], + 'G' => [ + 'D' => 15, ], ]; $scoreCache = []; @@ -112,7 +145,7 @@ protected function setUp(): void foreach ($scoresForTeam as $problemLabel => $minute) { $firstToSolve = in_array( $teamLabel . $problemLabel, - ['AA', 'BB', 'BC', 'CD'] + ['AA', 'BB', 'BC', 'CB', 'CC', 'DD'] ); $scoreCache[] = (new ScoreCache()) ->setContest($this->contest) @@ -164,15 +197,19 @@ public function testWinner(): void public function testMedals(): void { $medals = [ - 'gold' => 'team_A', - 'silver' => 'team_B', - 'bronze' => 'team_C', + 'gold' => ['team_A', 'team_B', 'team_C'], + 'silver' => [], + 'bronze' => ['team_G'], ]; - foreach ($medals as $medal => $team) { + foreach ($medals as $medal => $teams) { $medalAward = $this->getAward($medal . '-medal'); - static::assertNotNull($medalAward); - static::assertEquals(ucfirst($medal) . ' medal winner', $medalAward['citation']); - static::assertEquals([$team], $medalAward['team_ids']); + if (empty($teams)) { + static::assertNull($medalAward); + } else { + static::assertNotNull($medalAward); + static::assertEquals(ucfirst($medal) . ' medal winner', $medalAward['citation']); + static::assertEquals($teams, $medalAward['team_ids']); + } } } @@ -187,22 +224,29 @@ public function testGroupWinners(): void $groupBWinner = $this->getAward('group-winner-cat_B'); static::assertNotNull($groupBWinner); static::assertEquals('Winner(s) of group Category B', $groupBWinner['citation']); - static::assertEquals(['team_C'], $groupBWinner['team_ids']); + static::assertEquals(['team_D'], $groupBWinner['team_ids']); + + $a = $this->getAwardService()->getAwards($this->contest, $this->scoreboard); + $groupBWinner = $this->getAward('group-winner-cat_C'); + static::assertNotNull($groupBWinner); + static::assertEquals('Winner(s) of group Category C', $groupBWinner['citation']); + static::assertEquals(['team_G'], $groupBWinner['team_ids']); } public function testFirstToSolve(): void { $fts = [ - 'A' => 'A', - 'B' => 'B', - 'C' => 'B', - 'D' => 'C', + 'A' => ['A'], + 'B' => ['B', 'C'], + 'C' => ['B', 'C'], + 'D' => ['D'], ]; - foreach ($fts as $problem => $team) { + foreach ($fts as $problem => $teams) { $firstToSolve = $this->getAward('first-to-solve-problem_' . $problem); static::assertNotNull($firstToSolve); static::assertEquals('First to solve problem ' . $problem, $firstToSolve['citation']); - static::assertEquals(['team_' . $team], $firstToSolve['team_ids']); + $teamIds = array_map(static fn(string $team) => 'team_' . $team, $teams); + static::assertEquals($teamIds, $firstToSolve['team_ids']); } } @@ -219,8 +263,13 @@ public function testMedalType(int $teamIndex, ?string $expectedMedalType) public function provideMedalType(): \Generator { yield [0, 'gold-medal']; - yield [1, 'silver-medal']; - yield [2, 'bronze-medal']; + yield [1, 'gold-medal']; + yield [2, 'gold-medal']; yield [3, null]; + yield [4, null]; + yield [5, null]; + yield [6, 'bronze-medal']; + yield [7, null]; + yield [8, null]; } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy