diff --git a/.github/jobs/baseinstall.sh b/.github/jobs/baseinstall.sh index 3fe155a0cd7..55fdf0ccf21 100755 --- a/.github/jobs/baseinstall.sh +++ b/.github/jobs/baseinstall.sh @@ -1,54 +1,20 @@ #!/bin/sh -# Functions to annotate the Github actions logs -alias trace_on='set -x' -alias trace_off='{ set +x; } 2>/dev/null' - -section_start_internal () { - echo "::group::$1" - trace_on -} - -section_end_internal () { - echo "::endgroup::" - trace_on -} - -alias section_start='trace_off ; section_start_internal ' -alias section_end='trace_off ; section_end_internal ' +. .github/jobs/ci_settings.sh export version="$1" +db=${2:-install} set -eux -section_start "Update packages" -sudo apt update -section_end - -section_start "Install needed packages" -sudo apt install -y acl zip unzip nginx php php-fpm php-gd \ - php-cli php-intl php-mbstring php-mysql php-curl php-json \ - php-xml php-zip ntp make sudo debootstrap \ - libcgroup-dev lsof php-cli php-curl php-json php-xml \ - php-zip procps gcc g++ default-jre-headless \ - default-jdk-headless ghc fp-compiler autoconf automake bats \ - python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig \ - python3-yaml latexmk curl -section_end - PHPVERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION."\n";') export PHPVERSION -section_start "Install composer" -php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -HASH="$(wget -q -O - https://composer.github.io/installer.sig)" -php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" -sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer -section_end - section_start "Run composer" export APP_ENV="dev" -composer install --no-scripts +cd webapp +composer install --no-scripts |tee "$ARTIFACTS"/composer_out.txt +cd .. section_end section_start "Set simple admin password" @@ -58,59 +24,98 @@ section_end section_start "Install domserver" make configure -./configure --with-baseurl='https://localhost/domjudge/' --enable-doc-build=no --prefix="/opt/domjudge" +./configure \ + --with-baseurl='https://localhost/domjudge/' \ + --with-domjudge-user=root \ + --enable-doc-build=no \ + --enable-judgehost-build=no | tee "$ARTIFACTS"/configure.txt make domserver -sudo make install-domserver +make install-domserver section_end -section_start "Explicit start mysql + install DB" -sudo /etc/init.d/mysql start +section_start "SQL settings" +cat > ~/.my.cnf < /opt/domjudge/domserver/etc/dbpasswords.secret +mysql_user "SELECT CURRENT_USER();" +mysql_user "SELECT USER();" +section_end +section_start "Install DOMjudge database" /opt/domjudge/domserver/bin/dj_setup_database -uroot -proot bare-install section_end +section_start "Show PHP config" +php -v | tee -a "$ARTIFACTS"/php.txt +php -m | tee -a "$ARTIFACTS"/php.txt +section_end + +section_start "Show general config" +printenv | tee -a "$ARTIFACTS"/environment.txt +cp /etc/os-release "$ARTIFACTS"/os-release.txt +cp /proc/cmdline "$ARTIFACTS"/cmdline.txt +section_end + section_start "Setup webserver" -sudo cp /opt/domjudge/domserver/etc/domjudge-fpm.conf /etc/php/$PHPVERSION/fpm/pool.d/domjudge.conf +cp /opt/domjudge/domserver/etc/domjudge-fpm.conf /etc/php/"$PHPVERSION"/fpm/pool.d/domjudge.conf -sudo rm -f /etc/nginx/sites-enabled/* -sudo cp /opt/domjudge/domserver/etc/nginx-conf /etc/nginx/sites-enabled/domjudge +rm -f /etc/nginx/sites-enabled/* +cp /opt/domjudge/domserver/etc/nginx-conf /etc/nginx/sites-enabled/domjudge openssl req -nodes -new -x509 -keyout /tmp/server.key -out /tmp/server.crt -subj "/C=NL/ST=Noord-Holland/L=Amsterdam/O=TestingForPR/CN=localhost" -sudo cp /tmp/server.crt /usr/local/share/ca-certificates/ -sudo update-ca-certificates +cp /tmp/server.crt /usr/local/share/ca-certificates/ +update-ca-certificates # shellcheck disable=SC2002 -cat "$(pwd)/.github/jobs/data/nginx_extra" | sudo tee -a /etc/nginx/sites-enabled/domjudge -sudo nginx -t +cat "$(pwd)/.github/jobs/data/nginx_extra" | tee -a /etc/nginx/sites-enabled/domjudge +nginx -t section_end section_start "Show webserver is up" for service in nginx php${PHPVERSION}-fpm; do - sudo systemctl restart $service - sudo systemctl status $service + service "$service" restart + service "$service" status done section_end -section_start "Install the example data" -/opt/domjudge/domserver/bin/dj_setup_database -uroot -proot install-examples -section_end +if [ "${db}" = "install" ]; then + section_start "Install the example data" + /opt/domjudge/domserver/bin/dj_setup_database -uroot -proot install-examples | tee -a "$ARTIFACTS/mysql.txt" + section_end +fi section_start "Setup user" # We're using the admin user in all possible roles -echo "DELETE FROM userrole WHERE userid=1;" | mysql -uroot -proot domjudge +mysql_root "DELETE FROM userrole WHERE userid=1;" domjudge if [ "$version" = "team" ]; then # Add team to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 3);" | mysql -uroot -proot domjudge - echo "UPDATE user SET teamid = 1 WHERE userid = 1;" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 3);" domjudge + mysql_root "UPDATE user SET teamid = 1 WHERE userid = 1;" domjudge elif [ "$version" = "jury" ]; then # Add jury to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 2);" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 2);" domjudge elif [ "$version" = "balloon" ]; then # Add balloon to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 4);" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 4);" domjudge elif [ "$version" = "admin" ]; then # Add admin to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 1);" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 1);" domjudge fi section_end diff --git a/.github/jobs/ci_settings.sh b/.github/jobs/ci_settings.sh new file mode 100644 index 00000000000..350b07cf093 --- /dev/null +++ b/.github/jobs/ci_settings.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Store artifacts/logs +export ARTIFACTS="/tmp/artifacts" +mkdir -p "$ARTIFACTS" + +# Functions to annotate the Github actions logs +trace_on () { + set -x +} +trace_off () { + { + set +x + } 2>/dev/null +} + +section_start_internal () { + echo "::group::$1" + trace_on +} + +section_end_internal () { + echo "::endgroup::" + trace_on +} + +mysql_root () { + # shellcheck disable=SC2086 + echo "$1" | mysql -uroot -proot ${2:-} | tee -a "$ARTIFACTS"/mysql.txt +} + +mysql_user () { + # shellcheck disable=SC2086 + echo "$1" | mysql -udomjudge -pdomjudge ${2:-} | tee -a "$ARTIFACTS"/mysql.txt +} + +section_start () { + if [ "$#" -ne 1 ]; then + echo "Only 1 argument is needed for GHA, 2 was needed for GitLab." + exit 1 + fi + trace_off + section_start_internal "$1" +} + +section_end () { + trace_off + section_end_internal +} diff --git a/.github/jobs/composer_setup.sh b/.github/jobs/composer_setup.sh new file mode 100755 index 00000000000..5e5213e42bf --- /dev/null +++ b/.github/jobs/composer_setup.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -eux + +. .github/jobs/ci_settings.sh + +section_start "Configure PHP" +PHPVERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION."\n";') +export PHPVERSION +echo "$PHPVERSION" | tee -a "$ARTIFACTS"/phpversion.txt +section_end + +section_start "Run composer" +cd webapp +composer install --no-scripts 2>&1 | tee -a "$ARTIFACTS/composer_log.txt" +section_end diff --git a/.github/jobs/configure-checks/all.bats b/.github/jobs/configure-checks/all.bats index c7603ae266e..630773158c9 100755 --- a/.github/jobs/configure-checks/all.bats +++ b/.github/jobs/configure-checks/all.bats @@ -37,7 +37,7 @@ setup() { if [ "$distro_id" = "ID=fedora" ]; then repo-install httpd fi - repo-install gcc g++ libcgroup-dev + repo-install gcc g++ libcgroup-dev composer } run_configure () { @@ -129,14 +129,6 @@ compile_assertions_finished () { compile_assertions_finished } -@test "Install GNU C/C++ compilers" { - # The test above already does this - # repo-remove clang - # repo-install gcc g++ libcgroup-dev - # compiler_assertions - # compile_assertions_finished -} - @test "Install C/C++ compilers (Clang as alternative)" { if [ "$distro_id" = "ID=fedora" ]; then # Fedora has gcc as dependency for clang @@ -239,7 +231,6 @@ compile_assertions_finished () { assert_line " - bin..............: /opt/domjudge/domserver/bin" assert_line " - etc..............: /opt/domjudge/domserver/etc" assert_line " - lib..............: /opt/domjudge/domserver/lib" - assert_line " - libvendor........: /opt/domjudge/domserver/lib/vendor" assert_line " - log..............: /opt/domjudge/domserver/log" assert_line " - run..............: /opt/domjudge/domserver/run" assert_line " - sql..............: /opt/domjudge/domserver/sql" @@ -256,7 +247,6 @@ compile_assertions_finished () { assert_line " - tmp..............: /opt/domjudge/judgehost/tmp" assert_line " - judge............: /opt/domjudge/judgehost/judgings" assert_line " - chroot...........: /chroot/domjudge" - assert_line " - cgroup...........: /sys/fs/cgroup" } @test "Prefix configured" { @@ -266,7 +256,6 @@ compile_assertions_finished () { refute_line " * documentation.......: /opt/domjudge/doc" refute_line " * domserver...........: /opt/domjudge/domserver" refute_line " - bin..............: /opt/domjudge/domserver/bin" - refute_line " - libvendor........: /opt/domjudge/domserver/lib/vendor" refute_line " - tmp..............: /opt/domjudge/domserver/tmp" refute_line " - example_problems.: /opt/domjudge/domserver/example_problems" refute_line " * judgehost...........: /opt/domjudge/judgehost" @@ -278,7 +267,6 @@ compile_assertions_finished () { assert_line " * prefix..............: /tmp" assert_line " * documentation.......: /tmp/doc" assert_line " * domserver...........: /tmp/domserver" - assert_line " - libvendor........: /tmp/domserver/lib/vendor" assert_line " * judgehost...........: /tmp/judgehost" assert_line " - judge............: /tmp/judgehost/judgings" } @@ -300,7 +288,6 @@ compile_assertions_finished () { assert_line " - bin..............: /usr/local/bin" assert_line " - etc..............: /usr/local/etc/domjudge" assert_line " - lib..............: /usr/local/lib/domjudge" - assert_line " - libvendor........: /usr/local/lib/domjudge/vendor" assert_line " - log..............: /usr/local/var/log/domjudge" assert_line " - run..............: /usr/local/var/run/domjudge" assert_line " - sql..............: /usr/local/share/domjudge/sql" @@ -317,19 +304,17 @@ compile_assertions_finished () { assert_line " - tmp..............: /tmp" assert_line " - judge............: /usr/local/var/lib/domjudge/judgings" assert_line " - chroot...........: /chroot/domjudge" - assert_line " - cgroup...........: /sys/fs/cgroup" } @test "Alternative dirs together with FHS" { setup - run run_configure --enable-fhs --with-domserver_webappdir=/run/webapp --with-domserver_tmpdir=/tmp/domserver --with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot/domjudge --with-judgehost_cgroupdir=/sys/fs/altcgroup + run run_configure --enable-fhs --with-domserver_webappdir=/run/webapp --with-domserver_tmpdir=/tmp/domserver --with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot/domjudge assert_line " * prefix..............: /usr/local" assert_line " * documentation.......: /usr/local/share/doc/domjudge" assert_line " * domserver...........: " assert_line " - bin..............: /usr/local/bin" assert_line " - etc..............: /usr/local/etc/domjudge" assert_line " - lib..............: /usr/local/lib/domjudge" - assert_line " - libvendor........: /usr/local/lib/domjudge/vendor" assert_line " - log..............: /usr/local/var/log/domjudge" assert_line " - run..............: /usr/local/var/run/domjudge" assert_line " - sql..............: /usr/local/share/domjudge/sql" @@ -351,13 +336,11 @@ compile_assertions_finished () { assert_line " - judge............: /srv/judgings" refute_line " - chroot...........: /chroot/domjudge" assert_line " - chroot...........: /srv/chroot/domjudge" - refute_line " - cgroup...........: /sys/fs/cgroup" - assert_line " - cgroup...........: /sys/fs/altcgroup" } @test "Alternative dirs together with defaults" { setup - run run_configure "--with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot --with-judgehost_cgroupdir=/sys/fs/altcgroup --with-domserver_logdir=/log" + run run_configure "--with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot --with-domserver_logdir=/log" assert_line " * prefix..............: /opt/domjudge" assert_line " * documentation.......: /opt/domjudge/doc" assert_line " * domserver...........: /opt/domjudge/domserver" @@ -370,8 +353,6 @@ compile_assertions_finished () { assert_line " - judge............: /srv/judgings" refute_line " - chroot...........: /chroot/domjudge" assert_line " - chroot...........: /srv/chroot" - refute_line " - cgroup...........: /sys/fs/cgroup" - assert_line " - cgroup...........: /sys/fs/altcgroup" } @test "Default URL not set, docs mention" { diff --git a/.github/jobs/data/codespellignorefiles.txt b/.github/jobs/data/codespellignorefiles.txt index 41309979340..e94815b62b6 100644 --- a/.github/jobs/data/codespellignorefiles.txt +++ b/.github/jobs/data/codespellignorefiles.txt @@ -10,7 +10,7 @@ ./config.guess ./gitlab/codespell.yml ./.github/jobs/uploadcodecov.sh -./lib/vendor +./webapp/vendor ./webapp/public/bundles ./webapp/public/js/ace ./webapp/templates/bundles @@ -26,3 +26,4 @@ nv.d3.min* composer* ./doc/logos ./m4 +./webapp/tests/Unit/Fixtures diff --git a/.github/jobs/pa11y_config.json b/.github/jobs/pa11y_config.json new file mode 100644 index 00000000000..af7ef121515 --- /dev/null +++ b/.github/jobs/pa11y_config.json @@ -0,0 +1,9 @@ +{ + "chromeLaunchConfig": { + "args": [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage" + ] + } +} diff --git a/.github/jobs/syntax-check b/.github/jobs/syntax-check index 609846369f4..ac9d5a89503 100755 --- a/.github/jobs/syntax-check +++ b/.github/jobs/syntax-check @@ -23,7 +23,7 @@ if [ ! -x /usr/bin/shellcheck ]; then fi find . \( \ - -path ./lib/vendor -prune \ + -path ./webapp/vendor -prune \ -o -path ./webapp/var -prune \ -o -path ./output -prune \ -o -path ./.git -prune \ @@ -41,14 +41,14 @@ while read -r i ; do fi if grep -q "^#\\!.*/bin/sh" "$i" && \ [ "${i##*.}" != "zip" ] && \ - echo "$i" | grep -qvE '(^\./(misc-tools/dj_judgehost_cleanup.in|misc-tools/dj_make_chroot.in|config|autom4te|install-sh|sql/files/defaultdata/hs/run|sql/files/defaultdata/kt/run|lib/vendor/|output|judge/judgedaemon))'; then + echo "$i" | grep -qvE '(^\./(misc-tools/dj_judgehost_cleanup.in|misc-tools/dj_make_chroot.in|config|autom4te|install-sh|sql/files/defaultdata/hs/run|sql/files/defaultdata/kt/run|webapp/vendor/|output|judge/judgedaemon))'; then # shellcheck disable=SC2001 echo "$i" | sed -e 's|^./|check for bashisms: |' checkbashisms "$i" fi if grep -qE "^#\\!.*/bin/(ba)?sh" "$i" && \ [ "${i##*.}" != "zip" ] && \ - echo "$i" | grep -qvE '(^\./(config|autom4te|install-sh|sql/files/defaultdata/hs/run|lib/vendor/|output|judge/judgedaemon))'; then + echo "$i" | grep -qvE '(^\./(config|autom4te|install-sh|sql/files/defaultdata/hs/run|webapp/vendor/|output|judge/judgedaemon))'; then # We ignore the following shellcheck warnings, for more details see: # https://github.com/koalaman/shellcheck/wiki/ # diff --git a/.github/jobs/webstandard.sh b/.github/jobs/webstandard.sh new file mode 100755 index 00000000000..69f2191dd27 --- /dev/null +++ b/.github/jobs/webstandard.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +. .github/jobs/ci_settings.sh + +DIR="$PWD" + +if [ "$#" -ne "2" ]; then + exit 2 +fi + +TEST="$1" +ROLE="$2" + +cd /opt/domjudge/domserver + +section_start "Setup pa11y" +pa11y --version +section_end + +section_start "Setup the test user" +ADMINPASS=$(cat etc/initial_admin_password.secret) +export COOKIEJAR +COOKIEJAR=$(mktemp --tmpdir) +export CURLOPTS="--fail -sq -m 30 -b $COOKIEJAR" +if [ "$ROLE" = "public" ]; then + ADMINPASS="failedlogin" +fi + +# Make an initial request which will get us a session id, and grab the csrf token from it +CSRFTOKEN=$(curl $CURLOPTS -c $COOKIEJAR "http://localhost/domjudge/login" 2>/dev/null | sed -n 's/.*_csrf_token.*value="\(.*\)".*/\1/p') +# Make a second request with our session + csrf token to actually log in +# shellcheck disable=SC2086 +curl $CURLOPTS -c "$COOKIEJAR" -F "_csrf_token=$CSRFTOKEN" -F "_username=admin" -F "_password=$ADMINPASS" "http://localhost/domjudge/login" + +# Move back to the default directory +cd "$DIR" + +cp "$COOKIEJAR" cookies.txt +sed -i 's/#HttpOnly_//g' cookies.txt +sed -i 's/\t0\t/\t1999999999\t/g' cookies.txt +section_end + +# Could try different entrypoints +FOUNDERR=0 +URL=public +mkdir "$URL" +cd "$URL" +cp "$DIR"/cookies.txt ./ +section_start "Scrape the site with the rebuild admin user" +set +e +wget \ + --reject-regex logout \ + --recursive \ + --no-clobber \ + --page-requisites \ + --html-extension \ + --convert-links \ + --restrict-file-names=windows \ + --domains localhost \ + --no-parent \ + --load-cookies cookies.txt \ + http://localhost/domjudge/"$URL" +set -e +RET=$? +section_end + +section_start "Archive downloaded site" +cp -r localhost $ARTIFACTS/ +section_end + +section_start "Analyse failures" +#https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html +# Exit code 4 is network error which we can ignore +# Exit code 8 can also be because of HTTP404 or 400 +if [ $RET -ne 4 ] && [ $RET -ne 0 ] && [ $RET -ne 8 ]; then + exit $RET +fi + +EXPECTED_HTTP_CODES="200\|302\|400\|404\|403" +if [ "$ROLE" = "public" ]; then + # It's expected to encounter a 401 for the login page as we supply the wrong password + EXPECTED_HTTP_CODES="$EXPECTED_HTTP_CODES\|401" +fi +set +e +NUM_ERRORS=$(grep -v "HTTP/1.1\" \($EXPECTED_HTTP_CODES\)" /var/log/nginx/domjudge.log | grep -v "robots.txt" -c; if [ "$?" -gt 1 ]; then exit 127; fi) +set -e +echo "$NUM_ERRORS" + +if [ "$NUM_ERRORS" -ne 0 ]; then + grep -v "HTTP/1.1\" \($EXPECTED_HTTP_CODES\)" /var/log/nginx/domjudge.log | grep -v "robots.txt" + exit 1 +fi +section_end + +if [ "$TEST" = "w3cval" ]; then + section_start "Remove files from upstream with problems" + rm -rf localhost/domjudge/doc + rm -rf localhost/domjudge/css/fontawesome-all.min.css* + rm -rf localhost/domjudge/bundles/nelmioapidoc* + rm -f localhost/domjudge/css/bootstrap.min.css* + rm -f localhost/domjudge/css/select2-bootstrap*.css* + rm -f localhost/domjudge/css/dataTables*.css* + rm -f localhost/domjudge/jury/config/check/phpinfo* + section_end + + section_start "Install testsuite" + cd "$DIR" + wget https://github.com/validator/validator/releases/latest/download/vnu.linux.zip + unzip -q vnu.linux.zip + section_end + + FLTR='--filterpattern .*autocomplete.*|.*style.*|.*role=tab.*|.*descendant.*|.*Stray.*|.*attribute.*|.*Forbidden.*|.*stream.*|.*obsolete.*' + for typ in html css svg + do + section_start "Analyse with $typ" + # shellcheck disable=SC2086 + "$DIR"/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format json $FLTR "$URL" 2> result.json + # shellcheck disable=SC2086 + NEWFOUNDERRORS=$("$DIR"/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format gnu $FLTR "$URL" 2>&1 | wc -l) + FOUNDERR=$((NEWFOUNDERRORS+FOUNDERR)) + python3 -m "json.tool" < result.json > "$ARTIFACTS/w3c$typ$URL.json" + trace_off; python3 gitlab/jsontogitlab.py "$ARTIFACTS/w3c$typ$URL.json"; trace_on + section_end + done +else + section_start "Remove files from upstream with problems" + rm -rf localhost/domjudge/{doc,api} + section_end + + if [ "$TEST" == "axe" ]; then + STAN="-e $TEST" + FLTR="" + else + STAN="-s $TEST" + FLTR0="-E '#DataTables_Table_0 > tbody > tr > td > a','#menuDefault > a','#filter-card > div > div > div > span > span:nth-child(1) > span > ul > li > input',.problem-badge" + FLTR1="'html > body > div > div > div > div > div > div > table > tbody > tr > td > a > span','html > body > div > div > div > div > div > div > form > div > div > div > label'" + FLTR="$FLTR0,$FLTR1" + fi + chown -R domjudge:domjudge "$DIR" + cd "$DIR" + ACCEPTEDERR=5 + # shellcheck disable=SC2044,SC2035 + for file in $(find $URL -name "*.html") + do + section_start "$file" + su domjudge -c "pa11y --config .github/jobs/pa11y_config.json $STAN -r json -T $ACCEPTEDERR $FLTR $file" | python3 -m json.tool + ERR=$(su domjudge -c "pa11y --config .github/jobs/pa11y_config.json $STAN -r csv -T $ACCEPTEDERR $FLTR $file" | wc -l) + FOUNDERR=$((ERR+FOUNDERR-1)) # Remove header row + section_end + done +fi + +echo "Found: " $FOUNDERR +[ "$FOUNDERR" -eq 0 ] diff --git a/.github/workflows/autoconf-check-different-distro.yml b/.github/workflows/autoconf-check-different-distro.yml index 6c0cefc5b4b..8216eb3a26b 100644 --- a/.github/workflows/autoconf-check-different-distro.yml +++ b/.github/workflows/autoconf-check-different-distro.yml @@ -21,6 +21,6 @@ jobs: steps: - name: Install git so we get the .github directory run: dnf install -y git - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup image and run bats tests run: .github/jobs/configure-checks/setup_configure_image.sh diff --git a/.github/workflows/autoconf-check.yml b/.github/workflows/autoconf-check.yml index 5f8ed73f6b5..016ec024f71 100644 --- a/.github/workflows/autoconf-check.yml +++ b/.github/workflows/autoconf-check.yml @@ -20,10 +20,10 @@ jobs: exclude: - releaseBranch: false include: - - os: debian - version: testing - os: debian version: stable + - os: debian + version: testing runs-on: ubuntu-latest env: DEBIAN_FRONTEND: noninteractive @@ -33,6 +33,6 @@ jobs: steps: - name: Install git so we get the .github directory run: apt-get update; apt-get install -y git - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup image and run bats tests run: .github/jobs/configure-checks/setup_configure_image.sh diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b4c717d39a0..645826c1864 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,12 +12,14 @@ on: jobs: analyze: - # We can not run with our gitlab container - # CodeQL has missing .so files otherwise + container: + image: domjudge/gitlabci:24.04 + options: --user domjudge name: Analyze runs-on: ubuntu-latest env: COMPILED: "cpp" + USER: "domjudge" permissions: actions: read contents: read @@ -30,33 +32,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - - name: Install required tools - if: ${{ contains(env.COMPILED, matrix.language) }} - run: | - sudo apt update - sudo apt install -y acl zip unzip apache2 php php-fpm php-gd \ - php-cli php-intl php-mbstring php-mysql php-curl php-json \ - php-xml php-zip ntp make sudo debootstrap \ - libcgroup-dev lsof php-cli php-curl php-json php-xml \ - php-zip procps gcc g++ default-jre-headless \ - default-jdk-headless ghc fp-compiler autoconf automake bats \ - python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig \ - python3-yaml latexmk - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - HASH="$(wget -q -O - https://composer.github.io/installer.sig)" - php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" - sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer - - name: Install composer files if: ${{ contains(env.COMPILED, matrix.language) }} run: | + cd webapp composer install --no-scripts - name: Configure Makefile @@ -92,4 +78,4 @@ jobs: run: sudo chown -R ${USER} ./installdir - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 56c2dfac7ff..6ac2a9f5c8f 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -14,7 +14,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Rewrite Changelog to find new mistakes run: awk '1;/Version 7.2.1 - 6 May 2020/{exit}' ChangeLog > latest_Changelog - name: Get dirs to skip diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 9b3edc5990e..978bd4bb3d1 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -13,16 +13,16 @@ jobs: syntax-job: runs-on: ubuntu-latest container: - image: domjudge/gitlabci:2.1 + image: domjudge/gitlabci:24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run the syntax checks run: .github/jobs/syntax.sh detect-dump: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Search for leftover dump( statements" run: .github/jobs/detect_dump.sh @@ -31,7 +31,7 @@ jobs: container: image: pipelinecomponents/php-linter:latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Detect PHP linting issues run: > parallel-lint --colors @@ -49,10 +49,10 @@ jobs: image: pipelinecomponents/php-codesniffer:latest strategy: matrix: - PHPVERSION: ["8.1", "8.2"] + PHPVERSION: ["8.1", "8.2", "8.3"] steps: - run: apk add git - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Various fixes to this image run: .github/jobs/fix_pipelinecomponents_image.sh - name: Detect compatibility with supported PHP version diff --git a/.github/workflows/mayhem-api-template.yml b/.github/workflows/mayhem-api-template.yml deleted file mode 100644 index 445d6311c66..00000000000 --- a/.github/workflows/mayhem-api-template.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: "Mayhem API analysis (Template)" - -on: - workflow_call: - inputs: - version: - required: true - type: string - duration: - required: true - type: string - secrets: - MAPI_TOKEN: - required: true - -jobs: - mayhem: - name: Mayhem API analysis - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - env: - DB_DATABASE: domjudge - DB_USER: user - DB_PASSWORD: password - steps: - - uses: actions/checkout@v3 - - - name: Install DOMjudge - run: .github/jobs/baseinstall.sh ${{ inputs.version }} - - - name: Dump the OpenAPI - run: .github/jobs/getapi.sh - - - uses: actions/upload-artifact@v3 - if: ${{ inputs.version == 'guest' }} - with: - name: all-apispec - path: | - /home/runner/work/domjudge/domjudge/openapi.json - - - name: Mayhem for API - uses: ForAllSecure/mapi-action@v1 - if: ${{ inputs.version == 'guest' }} - continue-on-error: true - with: - mapi-token: ${{ secrets.MAPI_TOKEN }} - api-url: http://localhost/domjudge - api-spec: http://localhost/domjudge/api/doc.json # swagger/openAPI doc hosted here - duration: "auto" # Only spend time if we need to recheck issues from last time or find issues - sarif-report: mapi.sarif - run-args: | - --config - .github/jobs/data/mapi.config - --ignore-endpoint - ".*strict=true.*" - --ignore-endpoint - ".*strict=True.*" - - - name: Mayhem for API (For application role) - uses: ForAllSecure/mapi-action@v1 - if: ${{ inputs.version != 'guest' }} - continue-on-error: true - with: - mapi-token: ${{ secrets.MAPI_TOKEN }} - target: domjudge-${{ inputs.version }} - api-url: http://localhost/domjudge - api-spec: http://localhost/domjudge/api/doc.json # swagger/openAPI doc hosted here - duration: "${{ inputs.duration }}" - sarif-report: mapi.sarif - run-args: | - --config - .github/jobs/data/mapi.config - --basic-auth - admin:password - --ignore-endpoint - ".*strict=true.*" - --ignore-endpoint - ".*strict=True.*" - - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: mapi.sarif - - - uses: actions/upload-artifact@v3 - with: - name: ${{ inputs.version }}-logs - path: | - /var/log/nginx - /opt/domjudge/domserver/webapp/var/log/*.log diff --git a/.github/workflows/mayhem-daily.yml b/.github/workflows/mayhem-daily.yml deleted file mode 100644 index 2118bf6920c..00000000000 --- a/.github/workflows/mayhem-daily.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: "Mayhem API daily (admin role only)" - -on: - schedule: - - cron: '0 23 * * *' - -jobs: - mayhem-template: - uses: ./.github/workflows/mayhem-api-template.yml - with: - version: "admin" - duration: "auto" - secrets: - MAPI_TOKEN: ${{ secrets.MAPI_TOKEN }} diff --git a/.github/workflows/mayhem-weekly.yml b/.github/workflows/mayhem-weekly.yml deleted file mode 100644 index 71cc90ecbaf..00000000000 --- a/.github/workflows/mayhem-weekly.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: "Mayhem API weekly (all roles)" - -on: - schedule: - - cron: '0 23 * * 0' - -jobs: - mayhem-template: - strategy: - matrix: - include: - - version: "team" - duration: "5m" - - version: "guest" - duration: "auto" - - version: "jury" - duration: "5min" - - version: "admin" - duration: "10m" - uses: ./.github/workflows/mayhem-api-template.yml - with: - version: "${{ matrix.version }}" - duration: "${{ matrix.duration }}" - secrets: - MAPI_TOKEN: ${{ secrets.MAPI_TOKEN }} diff --git a/.github/workflows/phpcodesniffer.yml b/.github/workflows/phpcodesniffer.yml index 49f8f76f697..e733a25c3fd 100644 --- a/.github/workflows/phpcodesniffer.yml +++ b/.github/workflows/phpcodesniffer.yml @@ -14,7 +14,7 @@ jobs: phpcs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # important! - name: Install PHP_CodeSniffer diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index c550309f34e..b5772ef7110 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -13,11 +13,16 @@ jobs: phpstan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install DOMjudge - run: .github/jobs/baseinstall.sh admin + - uses: actions/checkout@v4 + - name: Setup composer dependencies + run: .github/jobs/composer_setup.sh - uses: php-actions/phpstan@v3 with: - configuration: phpstan.dist.neon + configuration: webapp/phpstan.dist.neon path: webapp/src webapp/tests php_extensions: gd intl mysqli pcntl zip + autoload_file: webapp/vendor/autoload.php + - uses: actions/upload-artifact@v4 + if: always() + with: + path: /tmp/artifacts diff --git a/.github/workflows/runpipe.yml b/.github/workflows/runpipe.yml index 551b8217576..415510c2c35 100644 --- a/.github/workflows/runpipe.yml +++ b/.github/workflows/runpipe.yml @@ -15,9 +15,9 @@ jobs: runpipe: runs-on: ubuntu-latest container: - image: domjudge/gitlabci:2.1 + image: domjudge/gitlabci:24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Create the configure file run: make configure - name: Do the default configure diff --git a/.github/workflows/shiftleft.yml b/.github/workflows/shiftleft.yml index be08719bffe..af212548e83 100644 --- a/.github/workflows/shiftleft.yml +++ b/.github/workflows/shiftleft.yml @@ -12,7 +12,7 @@ jobs: Scan-Build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Perform Scan uses: ShiftLeftSecurity/scan-action@master diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c4119722b7d..b6b6afd1a42 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -13,7 +13,7 @@ jobs: check-static-codecov: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download latest codecov upload script run: wget https://codecov.io/bash -O newcodecov - name: Detect changes to manually verify diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d9ad2b5b021..f5e87061a94 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,17 +1,15 @@ include: - '/gitlab/ci/unit.yml' - '/gitlab/ci/integration.yml' - - '/gitlab/ci/webstandard.yml' - '/gitlab/ci/template.yml' - '/gitlab/ci/misc.yml' stages: - test - integration - - accessibility - chroot_checks - unit - style - ci_checks -image: domjudge/gitlabci:22.04 +image: domjudge/gitlabci:24.04 diff --git a/ChangeLog b/ChangeLog index b32771f786e..f38584f8e53 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,12 @@ DOMjudge Programming Contest Judging System -Version 8.3.0DEV ----------------------------- +Version 8.4.0DEV +--------------------------- + - Get rid of 'internal' data source mode, always requiring - but auto + generating - external ID's for all entities to simplify event logic. + +Version 8.3.0 - 31 May 2024 +--------------------------- - [security] Close metadata file descriptor for the child in runguard. - Document that minimum PHP version is now 8.1.0. - Upgrade Symfony to 6.2 and upgrade other library dependencies. @@ -29,6 +34,30 @@ Version 8.3.0DEV - Add direct button to public page for registering when enabled. - Scale batch size of judge tasks dynamically. - Rename room to location for teams. + - Add option to upload contest problemset document. + - Improve on the advice for rejuding on config changes, only when relevant + advice to start a rejudging, similar for updating the scoreboard cache. + - For interactive problems: add explicit statement to when one of sides of + the interactions ran closed their output. + - Allow importing a (generic) contest warning which is displayed on the scoreboard. + - Added option in the dj_setup_database to make dumps, useful before major + upgrades and during the contest before problem imports. + - Fixed some bugs to become OpenAPI spec compliant again. + - Prevent a race condition leading to two submissions receiving the same + colour balloon. + - Compile executables with `-std=gnu++20` now. + - Record the testcase directory for judging runs, this makes investigating + easier. + - Set default C/C++ standards in configure script, we intend to ship all + scripts free of warnings/errors. + - Respect basic authentication in the URL for the import-contest when .netrc + exists. + - Fix CORS header for API to make extending with other tools easier. + - Fix eventfeed when SQL commits happen out of order to send all events. + - Lazy load the team pictures for the scoreboard, to prevent spamming access + log with HTTP404s + - Make TLE detection more robust for interactive problems. + - Added option for ICPC online scoreboard with no rank displays. Version 8.2.0 - 6 March 2023 ---------------------------- diff --git a/Makefile b/Makefile index df4f2069a03..108f6912d6b 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ export TOPDIR = $(shell pwd) REC_TARGETS=build domserver install-domserver judgehost install-judgehost \ - docs install-docs inplace-install inplace-uninstall + docs install-docs inplace-install inplace-uninstall maintainer-conf \ + maintainer-install composer-dependencies composer-dependencies-dev # Global Makefile definitions include $(TOPDIR)/Makefile.global @@ -47,7 +48,7 @@ endif domserver: domserver-configure paths.mk config judgehost: judgehost-configure paths.mk config docs: paths.mk config -install-domserver: domserver composer-dump-autoload domserver-create-dirs +install-domserver: domserver domserver-create-dirs install-judgehost: judgehost judgehost-create-dirs install-docs: docs-create-dirs dist: configure composer-dependencies @@ -64,27 +65,6 @@ ifneq "$(JUDGEHOST_BUILD_ENABLED)" "yes" @exit 1 endif -# Install PHP dependencies -composer-dependencies: -ifeq (, $(shell command -v composer 2> /dev/null)) - $(error "'composer' command not found in $(PATH), install it via your package manager or https://getcomposer.org/download/") -endif -# We use --no-scripts here because at this point the autoload.php file is -# not generated yet, which is needed to run the post-install scripts. - composer $(subst 1,-q,$(QUIET)) install --prefer-dist -o -a --no-scripts --no-plugins - -composer-dependencies-dev: - composer $(subst 1,-q,$(QUIET)) install --prefer-dist --no-scripts --no-plugins - -# Dump autoload dependencies (including plugins) -# This is needed since symfony/runtime is a Composer plugin that runs while dumping -# the autoload file -composer-dump-autoload: - composer $(subst 1,-q,$(QUIET)) dump-autoload -o -a - -composer-dump-autoload-dev: - composer $(subst 1,-q,$(QUIET)) dump-autoload - # Generate documentation for distribution. Remove this dependency from # dist above for quicker building from git sources. distdocs: @@ -100,19 +80,23 @@ build-scripts: $(MAKE) -C sql build-scripts # List of SUBDIRS for recursive targets: -build: SUBDIRS= lib misc-tools -domserver: SUBDIRS=etc sql misc-tools webapp -install-domserver: SUBDIRS=etc lib sql misc-tools webapp example_problems -judgehost: SUBDIRS=etc judge misc-tools -install-judgehost: SUBDIRS=etc lib judge misc-tools -docs: SUBDIRS= doc -install-docs: SUBDIRS= doc -inplace-install: SUBDIRS= doc misc-tools -inplace-uninstall: SUBDIRS= doc misc-tools -dist: SUBDIRS= lib sql misc-tools -clean: SUBDIRS=etc doc lib sql judge misc-tools webapp -distclean: SUBDIRS=etc doc lib sql judge misc-tools webapp -maintainer-clean: SUBDIRS=etc doc lib sql judge misc-tools webapp +build: SUBDIRS= lib misc-tools +domserver: SUBDIRS=etc sql misc-tools webapp +install-domserver: SUBDIRS=etc lib sql misc-tools webapp example_problems +judgehost: SUBDIRS=etc judge misc-tools +install-judgehost: SUBDIRS=etc lib judge misc-tools +docs: SUBDIRS= doc +install-docs: SUBDIRS= doc +maintainer-conf: SUBDIRS= webapp +maintainer-install: SUBDIRS= webapp +inplace-install: SUBDIRS= doc misc-tools webapp +inplace-uninstall: SUBDIRS= doc misc-tools +dist: SUBDIRS= lib sql misc-tools +clean: SUBDIRS=etc doc lib sql judge misc-tools webapp +distclean: SUBDIRS=etc doc lib sql judge misc-tools webapp +maintainer-clean: SUBDIRS=etc doc lib sql judge misc-tools webapp +composer-dependencies: SUBDIRS= webapp +composer-dependencies-dev: SUBDIRS= webapp domserver-create-dirs: $(INSTALL_DIR) $(addprefix $(DESTDIR),$(domserver_dirs)) @@ -167,6 +151,17 @@ endif # Fix permissions and ownership for password files: -$(INSTALL_USER) -m 0600 -t $(DESTDIR)$(judgehost_etcdir) \ etc/restapi.secret + @echo "" + @echo "========== Judgehost Install Completed ==========" + @echo "" + @echo "Optionally:" + @echo " - Install the create-cgroup service to setup the secure judging restrictions:" + @echo " cp judge/create-cgroups.service /etc/systemd/system/" + @echo " - Install the judgehost service:" + @echo " cp judge/domjudge-judgedaemon@.service /etc/systemd/system/" + @echo " - You can enable the judgehost on CPU core 1 with:" + @echo " systemctl enable domjudge-judgedaemon@1" + @echo "" check-root: @if [ `id -u` -ne 0 -a -z "$(QUIET)" ]; then \ @@ -193,7 +188,7 @@ paths.mk: @exit 1 # Configure for running in source tree, not meant for normal use: -maintainer-conf: inplace-conf-common composer-dependencies-dev webapp/.env.local +maintainer-conf: inplace-conf-common composer-dependencies-dev inplace-conf: inplace-conf-common composer-dependencies inplace-conf-common: dist ./configure $(subst 1,-q,$(QUIET)) --prefix=$(CURDIR) \ @@ -211,18 +206,11 @@ inplace-conf-common: dist --with-baseurl='http://localhost/domjudge/' \ $(CONFIGURE_FLAGS) -# Run Symfony in dev mode (for maintainer-mode): -webapp/.env.local: - @echo "Creating file '$@'..." - @echo "# This file was automatically created by 'make maintainer-conf' to run" > $@ - @echo "# the DOMjudge Symfony application in developer mode. Adjust as needed." >> $@ - @echo "APP_ENV=dev" >> $@ - # Install the system in place: don't really copy stuff, but create # symlinks where necessary to let it work from the source tree. # This stuff is a hack! -maintainer-install: inplace-install composer-dump-autoload-dev -inplace-install: build composer-dump-autoload domserver-create-dirs judgehost-create-dirs +maintainer-install: inplace-install +inplace-install: build domserver-create-dirs judgehost-create-dirs inplace-install-l: # Replace libjudgedir with symlink to prevent lots of symlinks: -rmdir $(judgehost_libjudgedir) @@ -239,8 +227,6 @@ inplace-install-l: # because judgehost-create-dirs sets wrong permissions: $(MKDIR_P) $(domserver_tmpdir) chmod a+rwx $(domserver_tmpdir) -# Make sure we're running from a clean state: - composer auto-scripts @echo "" @echo "========== Maintainer Install Completed ==========" @echo "" @@ -314,7 +300,7 @@ coverity-conf: coverity-build: paths.mk $(MAKE) build build-scripts # Secondly, delete all upstream PHP libraries to not analyze those: - -rm -rf lib/vendor/* + -rm -rf webapp/vendor/* @VERSION=` grep '^VERSION =' paths.mk | sed 's/^VERSION = *//'` ; \ PUBLISHED=`grep '^PUBLISHED =' paths.mk | sed 's/^PUBLISHED = *//'` ; \ if [ "$$PUBLISHED" = release ]; then DESC="release" ; \ @@ -341,5 +327,4 @@ clean-autoconf: $(addprefix inplace-,conf conf-common install uninstall) \ $(addprefix maintainer-,conf install) clean-autoconf config distdocs \ composer-dependencies composer-dependencies-dev \ - composer-dump-autoload composer-dump-autoload-dev \ coverity-conf coverity-build diff --git a/README.md b/README.md index 58b0fa0c773..e86c74860dd 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) [![CodeQL alerts](https://github.com/DOMjudge/domjudge/actions/workflows/codeql-analysis.yml/badge.svg?branch=main&event=push)](https://github.com/DOMjudge/domjudge/actions/workflows/codeql-analysis.yml) -This is the Programming Contest Jury System "DOMjudge" version 8.3.0DEV +This is the Programming Contest Jury System "DOMjudge" version 8.4.0DEV DOMjudge is a system for running a programming contest, like the ICPC regional and world championship programming contests. @@ -72,7 +72,7 @@ The M4 autoconf macros are licensed under all-permissive and GPL3+ licences; see the respective files under m4/ for details. The DOMjudge tarball ships external library dependencies in the -lib/vendor directory. These are covered by their individual licenses +webapp/vendor directory. These are covered by their individual licenses as specified in the file composer.lock. Contact diff --git a/SECURITY.md b/SECURITY.md index e67564f72de..ead247d37f0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,10 +4,10 @@ | DOMjudge Version | Supported | PHP version supported | |------------------| ------------------ |-----------------------| -| main branch | :warning: | 8.1-8.2 | +| main branch | :warning: | 8.1-8.3 | +| 8.3.x | :white_check_mark: | 8.1-8.3 | | 8.2.x | :white_check_mark: | 7.4-8.2 | -| 8.1.x | :white_check_mark: | 7.4-8.2 | -| < 8.1 | :x: | :x: | +| < 8.2 | :x: | :x: | ## Reporting a Vulnerability diff --git a/configure.ac b/configure.ac index 5e1bac15292..f5eed5c774b 100644 --- a/configure.ac +++ b/configure.ac @@ -170,7 +170,6 @@ if test "x$FHS_ENABLED" = xyes ; then AC_SUBST(domserver_webappdir, $datadir/${PACKAGE_TARNAME}/webapp) AC_SUBST(domserver_sqldir, $datadir/${PACKAGE_TARNAME}/sql) AC_SUBST(domserver_libdir, $libdir/${PACKAGE_TARNAME}) - AC_SUBST(domserver_libvendordir, $libdir/${PACKAGE_TARNAME}/vendor) AC_SUBST(domserver_logdir, $localstatedir/log/${PACKAGE_TARNAME}) AC_SUBST(domserver_rundir, $localstatedir/run/${PACKAGE_TARNAME}) AC_SUBST(domserver_tmpdir, /tmp) @@ -189,7 +188,6 @@ if test "x$FHS_ENABLED" = xyes ; then AC_SUBST(judgehost_tmpdir, /tmp) AC_SUBST(judgehost_judgedir, $localstatedir/lib/${PACKAGE_TARNAME}/judgings) AC_SUBST(judgehost_chrootdir, /chroot/${PACKAGE_TARNAME}) - AC_SUBST(judgehost_cgroupdir, /sys/fs/cgroup) fi AC_SUBST(domjudge_docdir, $docdir) @@ -223,7 +221,6 @@ AX_PATH(domserver_etcdir, [$domserver_root/etc]) AX_PATH(domserver_webappdir, [$domserver_root/webapp]) AX_PATH(domserver_sqldir, [$domserver_root/sql]) AX_PATH(domserver_libdir, [$domserver_root/lib]) -AX_PATH(domserver_libvendordir, [$domserver_root/lib/vendor]) AX_PATH(domserver_logdir, [$domserver_root/log]) AX_PATH(domserver_rundir, [$domserver_root/run]) AX_PATH(domserver_tmpdir, [$domserver_root/tmp]) @@ -241,32 +238,11 @@ AX_PATH(judgehost_rundir, [$judgehost_root/run]) AX_PATH(judgehost_tmpdir, [$judgehost_root/tmp]) AX_PATH(judgehost_judgedir, [$judgehost_root/judgings]) AX_PATH(judgehost_chrootdir, [/chroot/${PACKAGE_TARNAME}]) -AX_PATH(judgehost_cgroupdir, [/sys/fs/cgroup]) fi AX_WITH_COMMENT(7,[ ]) # }}} -# {{{ Directory for systemd unit files - -PKG_PROG_PKG_CONFIG() -AC_ARG_WITH([systemdsystemunitdir], - [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files])],, - [with_systemdsystemunitdir=auto]) -AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"], [ - AS_IF([test "x$PKG_CONFIG" = "x"],AC_MSG_ERROR([systemd support requested but no pkg-config available to query systemd package])) - def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) - - AS_IF([test "x$def_systemdsystemunitdir" = "x"], - [AS_IF([test "x$with_systemdsystemunitdir" = "xyes"], - [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) - with_systemdsystemunitdir=no], - [with_systemdsystemunitdir="$def_systemdsystemunitdir"])]) -AS_IF([test "x$with_systemdsystemunitdir" != "xno"], - [AC_SUBST([systemd_unitdir], [$with_systemdsystemunitdir])]) - -# }}} - AC_MSG_CHECKING([baseurl]) AC_ARG_WITH([baseurl], [AS_HELP_STRING([--with-baseurl=URL], [Base URL of the DOMjudge web interfaces. Example default: @@ -384,7 +360,6 @@ echo " * domserver...........: AX_VAR_EXPAND($domserver_root)" echo " - bin..............: AX_VAR_EXPAND($domserver_bindir)" echo " - etc..............: AX_VAR_EXPAND($domserver_etcdir)" echo " - lib..............: AX_VAR_EXPAND($domserver_libdir)" -echo " - libvendor........: AX_VAR_EXPAND($domserver_libvendordir)" echo " - log..............: AX_VAR_EXPAND($domserver_logdir)" echo " - run..............: AX_VAR_EXPAND($domserver_rundir)" echo " - sql..............: AX_VAR_EXPAND($domserver_sqldir)" @@ -405,11 +380,7 @@ echo " - run..............: AX_VAR_EXPAND($judgehost_rundir)" echo " - tmp..............: AX_VAR_EXPAND($judgehost_tmpdir)" echo " - judge............: AX_VAR_EXPAND($judgehost_judgedir)" echo " - chroot...........: AX_VAR_EXPAND($judgehost_chrootdir)" -echo " - cgroup...........: AX_VAR_EXPAND($judgehost_cgroupdir)" fi -echo "" -echo " * systemd unit files..: AX_VAR_EXPAND($systemd_unitdir)" -echo "" echo "Run 'make' without arguments to get a list of (build) targets." echo "" if test "x$BASEURL_UNCONFIGURED" = x1 ; then diff --git a/doc/logos/DOMjudgelogo-black-on-white.png b/doc/logos/DOMjudgelogo-black-on-white.png new file mode 100644 index 00000000000..2b3c31d50b7 Binary files /dev/null and b/doc/logos/DOMjudgelogo-black-on-white.png differ diff --git a/doc/logos/DOMjudgelogo-with-white-background.png b/doc/logos/DOMjudgelogo-with-white-background.png index 113bc65879b..090f36d2f98 100644 Binary files a/doc/logos/DOMjudgelogo-with-white-background.png and b/doc/logos/DOMjudgelogo-with-white-background.png differ diff --git a/doc/logos/DOMjudgelogo.png b/doc/logos/DOMjudgelogo.png index 108e4f95f4b..cfc4b93f991 100644 Binary files a/doc/logos/DOMjudgelogo.png and b/doc/logos/DOMjudgelogo.png differ diff --git a/doc/logos/DOMjudgelogo.svg b/doc/logos/DOMjudgelogo.svg index 75dde9b7ac3..bc3175b19d8 100644 --- a/doc/logos/DOMjudgelogo.svg +++ b/doc/logos/DOMjudgelogo.svg @@ -2,13 +2,6 @@ image/svg+xmlDOM + transform="matrix(-0.00528445,0.99706308,0.99998604,0.00519989,463.70996,56.28613)" + style="font-variant:normal;font-weight:bold;font-size:201.63111877px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-Bold;writing-mode:lr-tb;fill:#c1c4cb;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text41">DOM DOM + transform="matrix(-0.00528445,0.99706308,0.99998604,0.00519989,457.20703,62.77148)" + style="font-variant:normal;font-weight:bold;font-size:201.63111877px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-Bold;writing-mode:lr-tb;fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text45">DOM judge + transform="matrix(-0.00529202,0.99852359,1.001451,0.00520758,0,0)" + style="font-style:italic;font-variant:normal;font-weight:bold;font-size:71.296px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-BoldItalic;writing-mode:lr-tb;fill:#c1c4cb;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.998537" + id="text49" + x="591.8446" + y="366.89072">judge judge + transform="matrix(-0.00529202,0.99852359,1.001451,0.00520758,0,0)" + style="font-style:italic;font-variant:normal;font-weight:bold;font-size:71.296px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-BoldItalic;writing-mode:lr-tb;fill:#676e72;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.998537" + id="text53" + x="594.1568" + y="364.60156">judge / + d="M 212.91504,836.43604 187.46143,771.91748 H 168.37061 V 587.12256 H 153.25732 V 383.20996 H 136.5542 V 83.7168 h 89.8833" + style="fill:none;stroke:#231f20;stroke-width:1.65658998;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path55" + inkscape:connector-curvature="0" />/ / + transform="matrix(0.99862206,0,0,-1,195.41504,773.50928)" + style="font-variant:normal;font-weight:bold;font-size:29.47159004px;font-family:Courier;-inkscape-font-specification:CourierNewPS-BoldMT;writing-mode:lr-tb;fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text69">/ \ + transform="matrix(0.99862206,0,0,-1,213.71094,772.71338)" + style="font-variant:normal;font-weight:bold;font-size:29.47159004px;font-family:Courier;-inkscape-font-specification:CourierNewPS-BoldMT;writing-mode:lr-tb;fill:#c1c4cb;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text73">\ \ + transform="matrix(0.99862206,0,0,-1,212.91504,773.50928)" + style="font-variant:normal;font-weight:bold;font-size:29.47159004px;font-family:Courier;-inkscape-font-specification:CourierNewPS-BoldMT;writing-mode:lr-tb;fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text77">\ \ No newline at end of file + d="m 216.26563,587.27588 v 51.16846 c 0,30.1582 0.72656,59.14355 2.1875,86.96533 1.45312,27.81201 3.54687,41.72314 6.27343,41.72314 1.5625,0 2.88282,-4.10156 3.96875,-12.28418 1.07813,-8.18261 2.10938,-23.38525 3.09375,-45.58789 0.98438,-22.20263 1.47657,-45.81445 1.47657,-70.8164 v -51.16846 m -4,0.0195 v 51.12842 c 0,25.28857 -0.57813,47.40234 -1.73438,66.34131 -0.65625,10.45068 -1.58594,15.67578 -2.77344,15.67578 -1.15625,0 -2.10156,-5.75781 -2.83593,-17.26319 -1.10938,-17.43066 -1.65625,-39.01171 -1.65625,-64.7539 v -51.12842" + style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path79" + inkscape:connector-curvature="0" /> diff --git a/doc/manual/config-advanced.rst b/doc/manual/config-advanced.rst index 72b8016740c..9a471ac6ed3 100644 --- a/doc/manual/config-advanced.rst +++ b/doc/manual/config-advanced.rst @@ -16,30 +16,25 @@ path of your installation) as follows: - *Affiliation logos*: these will be shown with the teams that are part of the affiliation, if the ``show_affiliation_logos`` configuration option is enabled. They can be placed in - `public/images/affiliations/1234.png` where *1234* is the numeric ID + `public/images/affiliations/1234.png` where *1234* is the :ref:`external ID ` of the affiliation as shown in the DOMjudge interface. There is a separate option ``show_affiliations`` that independently controls where the affiliation *names* are shown on the scoreboard. These logos should be square and be at least 64x64 pixels, but not much bigger. - *Team pictures*: a photo of the team will be shown in the team details page if `public/images/teams/456.jpg` exists, where *456* is the - team's numeric ID as shown in the DOMjudge interface. DOMjudge will not + team's :ref:`external ID ` as shown in the DOMjudge interface. DOMjudge will not modify the photos in any way or form, so make sure you don't upload photos that are too big, since that will incur a lot of network traffic. - *Contest Banners*: a page-wide banner can be shown on the public scoreboard if that image is placed in `public/images/banners/1.png` where *1* is the - contest's numeric ID as shown in the DOMjudge interface. Alternatively, you + contest's :ref:`external ID ` as shown in the DOMjudge interface. Alternatively, you can place a file at `public/images/banner.png` which will be used as a banner for all contests. Contest-specific banners always have priority. Contest banners usually are rectangular, having a width of around 1920 pixels and a height of around 300 pixels. Other ratio's and sizes are supported, but check the public scoreboard to see how it looks. -.. note:: - - The IDs for affiliations, teams and contests need to be the *external ID* - if the ``data_source`` setting of DOMjudge is set to external. - It is also possible to load custom CSS and/or JavaScript files. To do so, place files ending in `.css` under `public/css/custom/` and/or files ending in `.js` under `public/js/custom/`. See the Config checker in the admin interface for the diff --git a/doc/manual/develop.rst b/doc/manual/develop.rst index e5912f6a630..13f41477c9c 100644 --- a/doc/manual/develop.rst +++ b/doc/manual/develop.rst @@ -39,11 +39,6 @@ already listed under :ref:`judgehost ` and :ref:`submit client ` requirements):: - sudo apt install autoconf automake bats \ - python-sphinx python-sphinx-rtd-theme rst2pdf fontconfig python3-yaml latexmk - -On Debian 11 (Bullseye) and above, instead install:: - sudo apt install autoconf automake bats \ python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig python3-yaml \ latexmk texlive-latex-recommended texlive-latex-extra tex-gyre diff --git a/doc/manual/import.rst b/doc/manual/import.rst index 2b685943b4f..b727551c461 100644 --- a/doc/manual/import.rst +++ b/doc/manual/import.rst @@ -13,6 +13,22 @@ installation. To use the CLI, you need to replace ```` with the path to the ``webapp`` directory of the DOMserver. +.. _external-ids: + +External ID's +------------- + +Most entities in DOMjudge have an `eternal ID`. External ID's are used to link +entities in DOMjudge to entities in an external system, e.g. the ICPC CMS. The +API uses the external ID's to expose entities to other systems. When you create +an entity in DOMjudge, specifying the external ID is optional; DOMjudge will +use an automatically generated ID if you don't provide one. However, if this ID +is not unique, you will get a message telling you that you need to provide your +own external ID. + +When importing entities in bulk as described below, the external ID will be +populated with the ID as specified in the files you import. + Importing team categories ------------------------- @@ -33,9 +49,7 @@ fields: - ``sortorder`` (defaults to ``0``): the sort order of the team category to use on the scoreboard. Categories with the same sortorder will be grouped together. -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the -ID used for the group. Otherwise, it will be exposed as ``externalid`` and a group ID will be -generated by DOMjudge. +The ``id`` field will be the ID used for the group. Example ``groups.json``:: @@ -73,9 +87,7 @@ Each of the following lines must contain the following elements separated by tab - the category ID. Must be unique - the name of the team category as shown on the scoreboard -If the ``data_source`` setting of DOMjudge is set to external, the category ID field will be -the ID used for the group. Otherwise, it will be exposed as ``externalid`` and a group ID will -be generated by DOMjudge. +The ``id`` field will be the ID used for the group. Example ``groups.tsv``:: @@ -114,9 +126,7 @@ fields: - ``formal_name``: the affiliation name as used on the scoreboard - ``country``: the country code in form of ISO 3166-1 alpha-3 -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the -ID used for the affiliation. Otherwise, it will be exposed as ``externalid`` and an affiliation -ID will be generated by DOMjudge. +The ``id`` field will be the ID used for the affiliation. Example ``organizations.json``:: @@ -168,11 +178,8 @@ fields: - ``organization_id``: the ID of the team affiliation this team belongs to - ``location.description`` (optional): the location of the team -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the -ID used for the team and the ``group_ids`` and ``organization_id`` fields are the values as -provided during the import of the other files listed above. Otherwise, the ``id`` will be -exposed as ``externalid``, a team ID will be generated by DOMjudge and you need to use the -ID's as generated by DOMjudge for ``group_ids`` as well as ``organization_id``. +The ``id`` field will be the ID used for the team and the ``group_ids`` and ``organization_id`` +fields are the values as provided during the import of the other files listed above. Example ``teams.json``:: @@ -221,11 +228,8 @@ Each of the following lines must contain the following elements separated by tab - a country code in form of ISO 3166-1 alpha-3 - an external institution ID, e.g. from the ICPC CMS, may be empty -If the ``data_source`` setting of DOMjudge is set to external, the team ID field will be the -ID used for the team and the category ID field is the value as provided during the import of -the other files listed above. Otherwise, the team ID will be exposed as ``externalid``, a -team ID will be generated by DOMjudge and you need to use the ID as generated by DOMjudge -for the category ID. +The team `id` field will be the ID used for the team and the category ID field is the value +as provided during the import of the other files listed above. Example ``teams2.tsv``:: @@ -265,10 +269,8 @@ fields: - ``name``: (optional) the full name of the account - ``ip`` (optional): IP address to link to this account -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the ID -used for the user and the ``team_id`` field is the value as provided during the team import. -Otherwise, the ``id`` will be exposed as ``externalid``, a user ID will be generated by DOMjudge -and you need to use the ID as generated by DOMjudge for ``team_id``. +The ``id`` field will be the ID used for the user and the ``team_id`` field is the value as provided during +the team import. Example ``accounts.yaml``:: diff --git a/doc/manual/index.rst b/doc/manual/index.rst index 2af6aeffa86..1ba568179cc 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -26,3 +26,4 @@ Appendices problem-format shadow configuration-reference + install-language diff --git a/doc/manual/install-domserver.rst b/doc/manual/install-domserver.rst index 46953405198..16ede690d0c 100644 --- a/doc/manual/install-domserver.rst +++ b/doc/manual/install-domserver.rst @@ -128,7 +128,7 @@ settings. Reload the web server for changes to take effect. An nginx webserver configuration snippet is also provided in ``etc/nginx-conf``. You still need ``htpasswd`` from ``apache2-utils`` -though. To use this configuration, perform the following steps:: +though. To use this configuration, perform the following steps: .. parsed-literal:: @@ -139,7 +139,7 @@ though. To use this configuration, perform the following steps:: service php\ |phpversion|-fpm reload service nginx reload -On Fedora, use the following nginx configuration steps:: +On Fedora, use the following nginx configuration steps: .. parsed-literal:: diff --git a/doc/manual/install-judgehost.rst b/doc/manual/install-judgehost.rst index fafe28c7f83..4066cad4ee6 100644 --- a/doc/manual/install-judgehost.rst +++ b/doc/manual/install-judgehost.rst @@ -68,6 +68,15 @@ example to install DOMjudge in the directory ``domjudge`` under `/opt`:: make judgehost sudo make install-judgehost +Example service files for the judgehost and the judgedaemon are provided in +``judge/create-cgroups.service`` and ``judge/domjudge-judgedaemon@.service``. The rest of the manual assumes you install those +in a location which is picked up by ``systemd``, for example ``/etc/systemd/system``. + +.. parsed-literal:: + + cp judge/domjudge-judgedaemon@.service /etc/systemd/system/ + cp judge/create-cgroups.service /etc/systemd/system/ + The judgedaemon can be run on various hardware configurations; - A virtual machine, typically these have 1 or 2 cores and no hyperthreading, because the kernel will schedule its own tasks on CPU 0, we advice CPU 1, diff --git a/doc/manual/install-language.rst b/doc/manual/install-language.rst new file mode 100644 index 00000000000..61ec9b25576 --- /dev/null +++ b/doc/manual/install-language.rst @@ -0,0 +1,97 @@ +Appendix: Installing the example languages +========================================== + +DOMjudge ships with some default languages with a default configuration. +As you might set up contests with those languages we provide how those languages were +installed in the past as guideline. Use ``dj_run_chroot`` for most of those packages, and see +the section :ref:`make-chroot` for more information. + +Most of the languages can be installed from the table below as there is a package available +to install inside the judging chroot. Given that you can install your own chroot we only provide the +packages for Ubuntu as that is the most used at the moment of writing. + +.. list-table:: Packages for languages + :header-rows: 1 + + * - Language + - Ubuntu package + - Remarks + * - Ada + - `gnat` + - + * - AWK + - `mawk`/`gawk` + - `mawk` is default installed + * - Bash + - `bash` + - Default installed in the chroot + * - C + - `gcc` + - Default installed in the chroot + * - C++ + - `g++` + - Default installed in the chroot + * - C# + - `mono-mcs` + - + * - Fortran + - `gfortran` + - + * - Haskell + - `ghc` + - After installing you need to move these files + `/{usr->var}/lib/ghc/package.conf.d` as `/var` + is not mounted during compilation. + * - Java + - `default-jdk-headless` + - Default installed in the chroot + * - Javascript + - `nodejs` + - + * - Kotlin + - `kotlin` + - + * - Lua + - `lua5.4` + - Ubuntu does not ship a generic meta package (yet). + * - Pascal + - `fp-compiler` + - + * - Perl + - `perl-base` + - Default installed in the chroot + * - POSIX shell + - `dash` + - Default installed in the chroot + * - Prolog + - `swi-prolog-core-packages` + - + * - Python3 + - `pypy3`/`python3` + - Default installed in the chroot. + DOMjudge assumes `pypy3` as it runs faster in general. + Consider the `PyPy3 PPA`_ if you need the latest python3 features. PyPy3 does not have 100% + compatibility with all non-standard libraries. In case this is needed you should reconsider the default + CPython implementation. + * - OCaml + - `ocaml` + - + * - R + - `r-base-core` + - + * - Ruby + - `ruby` + - + * - Rust + - `rustc` + - + * - Scala + - `scala` + - + * - Swift + - + - See the `Swift instructions`_, unpack the directory in the chroot and install `libncurses6`. Depending + on where you install the directory you might need to extend the `PATH` in the `run` script. + +.. _PyPy3 PPA: https://launchpad.net/~pypy/+archive/ubuntu/ppa +.. _Swift instructions: https://www.swift.org/documentation/server/guides/deploying/ubuntu.html diff --git a/doc/manual/install-workstation.rst b/doc/manual/install-workstation.rst index 1b862cb7d79..de51ea9ebbb 100644 --- a/doc/manual/install-workstation.rst +++ b/doc/manual/install-workstation.rst @@ -84,13 +84,10 @@ When DOMjudge is configured and site-specific configuration set, the team manual can be generated with the command ``make docs``. The following should do it on a Debian-like system:: - sudo apt install python-sphinx python-sphinx-rtd-theme rst2pdf fontconfig python3-yaml + sudo apt install python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig python3-yaml cd /doc/ make docs -On Debian 11 and above, install -``python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig python3-yaml`` instead. - The resulting manual will then be found in the ``team/`` subdirectory. .. _netrc manual page: https://ec.haxx.se/usingcurl/usingcurl-netrc diff --git a/doc/manual/problem-format.rst b/doc/manual/problem-format.rst index e9f6cbc3008..fd2d7990bdd 100644 --- a/doc/manual/problem-format.rst +++ b/doc/manual/problem-format.rst @@ -44,14 +44,16 @@ interface): - ``special_compare`` - executable id of a special compare script - ``points`` - number of points for this problem (defaults to 1) - ``color`` - CSS color specification for this problem - -The basename of the ZIP-file will be used as the problem short name (e.g. "A"). -All keys are optional. If they are present, the respective value will be -overwritten; if not present, then the value will not be changed or a default -chosen when creating a new problem. Test data files are added to set of test -cases already present. Thus, one can easily add test cases to a configured -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 + - ``externalid`` - the external id of the problem + - ``short-name`` - the short name of the problem + +The basename of the ZIP-file will be used as the problem external id and short +name (e.g. "A"). All keys are optional. If they are present, the respective +value will be overwritten; if not present, then the value will not be changed +or a default chosen when creating a new problem. Test data files are added to +set of test cases already present. Thus, one can easily add test cases to a +configured 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/legacy-icpc diff --git a/doc/manual/shadow.rst b/doc/manual/shadow.rst index d5e9f0d8506..986d3e2695c 100644 --- a/doc/manual/shadow.rst +++ b/doc/manual/shadow.rst @@ -19,11 +19,9 @@ Configuring DOMjudge In the DOMjudge admin interface, go to *Configuration settings* page and modify the settings to mimic the system to shadow from. Also make sure to set -*data_source* to ``configuration and live data external``. This tells DOMjudge +*shadow_mode* to ``true``. This tells DOMjudge that it will be a shadow for an external system. This will: -* Expose external ID's in the API for both configuration and live data, i.e. - problems, teams, etc. as well as submissions, judgings and runs. * Add a *Shadow Differences* and *External Contest Sources* item to the jury menu and homepage for admins. * Expose additional information in the submission overview and detail pages. diff --git a/doc/manual/team-overview.png b/doc/manual/team-overview.png index e2f678b438e..bed144a5bd6 100644 Binary files a/doc/manual/team-overview.png and b/doc/manual/team-overview.png differ diff --git a/doc/manual/team-scoreboard.png b/doc/manual/team-scoreboard.png index ea1afa1b6e4..2ba704fdf17 100644 Binary files a/doc/manual/team-scoreboard.png and b/doc/manual/team-scoreboard.png differ diff --git a/docker-compose.yml b/docker-compose.yml index 112065a42fa..181596b96ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,6 @@ # It is recommended to use `docker compose up` to start this stack. Note, don't # use sudo or the legacy docker-compose. -version: '3' - services: mariadb: image: docker.io/mariadb diff --git a/etc/db-config.yaml b/etc/db-config.yaml index 44eac76bc9f..665a48a6e83 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -111,16 +111,9 @@ type: int default_value: 50000 public: false - description: Maximum size of error/system output stored in the database (in bytes); use `-1` to disable any limits. - regex: /^[1-9]\d*$/ - error_message: A positive number is required. - - name: output_display_limit - type: int - default_value: 2000 - public: false - description: Maximum size of run/diff/error/system output shown in the jury interface (in bytes); use `-1` to disable any limits. - regex: /^[1-9]\d*$/ - error_message: A positive number is required. + description: Maximum size of error/system output stored in the database (in bytes); use `-1` to disable any limits. See `Display` / `output_display_limit` for how to control the output *shown*. + regex: /^([1-9]\d*|-1)$/ + error_message: A positive number or -1 is required. - name: lazy_eval_results type: int default_value: 1 @@ -208,6 +201,13 @@ - category: Display description: Options related to the DOMjudge user interface. items: + - name: output_display_limit + type: int + default_value: 2000 + public: false + description: Maximum size of run/diff/error/system output shown in the jury interface (in bytes); use `-1` to disable any limits. + regex: /^([1-9]\d*|-1)$/ + error_message: A positive number or -1 is required. - name: show_pending type: bool default_value: true @@ -318,6 +318,11 @@ 2: After first submission regex: /^\d+$/ error_message: A value between 0 and 2 is required. + - name: enable_ranking + type: bool + default_value: true + public: true + description: If disabled, no ranking information is shown to contestants. - category: Authentication description: Options related to authentication. items: @@ -350,17 +355,11 @@ enum_class: App\Utils\EventFeedFormat public: false description: Format of the event feed to use. See [current draft](https://ccs-specs.icpc.io/draft/contest_api#event-feed) and [versions available](https://ccs-specs.icpc.io/). - - name: data_source - type: int - default_value: 0 + - name: shadow_mode + type: bool + default_value: false public: false - description: "Source of data: used to indicate whether internal or external IDs are exposed in the API. `configuration data external` is typically used when loading configuration data from the ICPC CMS, and `configuration and live data external` when running DOMjudge as \"shadow system\"." - options: - 0: all local - 1: configuration data external - 2: configuration and live data external - regex: /^\d+$/ - error_message: A value between 0 and 2 is required. + description: Is this system running as a shadow system? docdescription: See :doc:`the chapter on running DOMjudge as a shadow system` for more information. - name: external_contest_sources_allow_untrusted_certificates type: bool diff --git a/etc/domserver-static.php.in b/etc/domserver-static.php.in index 41813091674..68967266b4f 100644 --- a/etc/domserver-static.php.in +++ b/etc/domserver-static.php.in @@ -7,14 +7,14 @@ define('DOMJUDGE_VERSION', '@DOMJUDGE_VERSION@'); -define('BINDIR', '@domserver_bindir@'); -define('ETCDIR', '@domserver_etcdir@'); -define('WEBAPPDIR', '@domserver_webappdir@'); -define('LIBDIR', '@domserver_libdir@'); -define('SQLDIR', '@domserver_sqldir@'); -define('LIBVENDORDIR','@domserver_libvendordir@'); -define('LOGDIR', '@domserver_logdir@'); -define('RUNDIR', '@domserver_rundir@'); -define('TMPDIR', '@domserver_tmpdir@'); +define('BINDIR', '@domserver_bindir@'); +define('ETCDIR', '@domserver_etcdir@'); +define('WEBAPPDIR', '@domserver_webappdir@'); +define('LIBDIR', '@domserver_libdir@'); +define('SQLDIR', '@domserver_sqldir@'); +define('VENDORDIR', '@domserver_webappdir@/vendor'); +define('LOGDIR', '@domserver_logdir@'); +define('RUNDIR', '@domserver_rundir@'); +define('TMPDIR', '@domserver_tmpdir@'); -define('BASEURL', '@BASEURL@'); +define('BASEURL', '@BASEURL@'); diff --git a/etc/judgehost-static.php.in b/etc/judgehost-static.php.in index f377d48d46f..602b81445c8 100644 --- a/etc/judgehost-static.php.in +++ b/etc/judgehost-static.php.in @@ -16,7 +16,6 @@ define('RUNDIR', '@judgehost_rundir@'); define('TMPDIR', '@judgehost_tmpdir@'); define('JUDGEDIR', '@judgehost_judgedir@'); define('CHROOTDIR', '@judgehost_chrootdir@'); -define('CGROUPDIR', '@judgehost_cgroupdir@'); define('RUNUSER', '@RUNUSER@'); define('RUNGROUP', '@RUNGROUP@'); diff --git a/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-submissions.R b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-submissions.R new file mode 120000 index 00000000000..74404bdb3e7 --- /dev/null +++ b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-submissions.R @@ -0,0 +1 @@ +fltcmp-test-write-files-between-testcases.R \ No newline at end of file diff --git a/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-testcases.R b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-testcases.R new file mode 100755 index 00000000000..fc9a2fac883 --- /dev/null +++ b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-testcases.R @@ -0,0 +1,73 @@ +# This checks if we can write extra files +# between different testcase runs. R uses +# a temporary directory which DOMjudge should +# make unavailable between runs and in case this +# is forgotten teams can precalculate extra files for later +# testcases. We need a working solution to make sure +# we get to all testcases. +# +# This submission is symlinked to make sure we submit it twice +# to verify that we can't see the TMPDIR of the earlier submission. +# +# The script will be WRONG-ANSWER if any of the assumptions +# are wrong. +# The script will be NO-OUTPUT if there is a security violation. +# The verdict RUN-ERROR is expected if we correctly detect the +# violation and would be the correct verdict if we don't need a +# TMPDIR. +# +# @EXPECTED_RESULTS@: CORRECT + +# For R the TMPDIR needs to be set, if it's not +# we can skip this whole test. +tmpdir<-Sys.getenv(c("TMPDIR")) +if (tmpdir == '') { + print("TMPDIR not set, update installation instructions.") + quit() +} + +# We had 3 testcases in the past, try with double for when new testcases get added. +testcaseIds<-rep(1:6) +possibleTempDirs <- (function (x) sprintf("/testcase%05d/write_tmp", x))(testcaseIds) +if (!(tmpdir %in% possibleTempDirs)) { + print("Either TMPDIR format has changed, or too many testcases.") + print(sprintf("Current TMPDIR: %s", tmpdir)) + quit() +} + +for (possibleTempDir in possibleTempDirs) { + teamFile<-sprintf("%s/fileFromTeam.txt", possibleTempDir) + if (file.exists(teamFile)) { + # File should not be here + quit() + } +} + +# Try to write to our TMPDIR to read it in the next testcase. +currentTeamFile<-sprintf("%s/fileFromTeam.txt", tmpdir) +fileConn<-file(currentTeamFile) +writeLines(c("Calculate","all","floats"), fileConn) +close(fileConn) +# Make sure our test from earlier does work for this testcase +if (!(file.exists(currentTeamFile))) { + # File should be here + quit() +} + +# Now try the actual problem to advance to the next testcase. +input<-file("stdin") +lines<-readLines(input) + +# https://evolutionarygenetics.github.io/Chapter10.html +# a simple reciprocal example +reciprocal <- function(x){ + # calculate the reciprocal + y <- 1/x + return(y) +} + +for (l in lines[-1]) { + l<-as.numeric(l) + r<-reciprocal(l) + cat(r,"\n") +} diff --git a/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk b/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk index de2d0a8c250..e41d032f7f8 100644 --- a/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk +++ b/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk @@ -3,4 +3,4 @@ # # @EXPECTED_RESULTS@: CORRECT -BEGIN { if ( DOMJUDGE ) print "Hello world!"; else print "variable DOMJUDGE not set" } +BEGIN { print "Hello world!" } diff --git a/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs b/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs index a70e0aae180..03133239e9c 100644 --- a/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs +++ b/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs @@ -11,10 +11,6 @@ public class Hello { public static void Main(string[] args) { -#if ONLINE_JUDGE Console.Write("Hello world!\n"); -#else - Console.Write("ONLINE_JUDGE not defined\n"); -#endif } } diff --git a/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 b/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 index 85294d89578..58dc8e9b1bd 100644 --- a/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 +++ b/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 @@ -5,10 +5,6 @@ ! @EXPECTED_RESULTS@: CORRECT ! program hello -#ifdef DOMJUDGE - write(*,"(A)") "Hello world!" -#else - write(*,"(A)") "DOMJUDGE not defined" -#endif +write(*,"(A)") "Hello world!" end program hello diff --git a/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js b/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js index a984f6dc1f8..46450611970 100644 --- a/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js +++ b/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js @@ -3,9 +3,4 @@ // // @EXPECTED_RESULTS@: CORRECT -if ( process.env.DOMJUDGE ) { - console.log('Hello world!'); -} else { - console.log('DOMJUDGE not defined'); - process.exit(1); -} +console.log('Hello world!'); diff --git a/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb b/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb index d7e30f58f69..e4b09af8aec 100644 --- a/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb +++ b/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb @@ -3,9 +3,4 @@ # # @EXPECTED_RESULTS@: CORRECT -if ENV['DOMJUDGE'] != '' then - puts "Hello world!" -else - puts "DOMJUDGE not defined" - exit(1) -end +puts "Hello world!" diff --git a/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh b/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh index 94b887c8e1f..8afe60aaa63 100644 --- a/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh +++ b/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh @@ -3,11 +3,6 @@ # # @EXPECTED_RESULTS@: CORRECT -if [ -z "$DOMJUDGE" -o -z "$ONLINE_JUDGE" ]; then - echo "Variable DOMJUDGE and/or ONLINE_JUDGE not defined." - exit 1 -fi - echo "Hello world!" exit 0 diff --git a/example_problems/hello/submissions/accepted/multifile-js-2/hello.js b/example_problems/hello/submissions/accepted/multifile-js-2/hello.js new file mode 100644 index 00000000000..7966b333a25 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js-2/hello.js @@ -0,0 +1,7 @@ +// This should give CORRECT on the default problem 'hello'. +// It does include another file for the actual implementation +// +// @EXPECTED_RESULTS@: CORRECT + +import { hello } from './module.js'; +console.log(hello()); diff --git a/example_problems/hello/submissions/accepted/multifile-js-2/module.js b/example_problems/hello/submissions/accepted/multifile-js-2/module.js new file mode 100644 index 00000000000..94f777f1c90 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js-2/module.js @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/accepted/multifile-js-2/package.json b/example_problems/hello/submissions/accepted/multifile-js-2/package.json new file mode 100644 index 00000000000..6990891ff35 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js-2/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/accepted/multifile-js/main.mjs b/example_problems/hello/submissions/accepted/multifile-js/main.mjs new file mode 100644 index 00000000000..a02377b4ecb --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js/main.mjs @@ -0,0 +1,7 @@ +// This should give CORRECT on the default problem 'hello'. +// It does include another file for the actual implementation +// +// @EXPECTED_RESULTS@: CORRECT + +import { hello } from './module.mjs'; +console.log(hello()); diff --git a/example_problems/hello/submissions/accepted/multifile-js/module.mjs b/example_problems/hello/submissions/accepted/multifile-js/module.mjs new file mode 100644 index 00000000000..94f777f1c90 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js/module.mjs @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-2/hello.js b/example_problems/hello/submissions/compiler_error/multifile-js-2/hello.js new file mode 100644 index 00000000000..5f9e2f846fb --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-2/hello.js @@ -0,0 +1,7 @@ +// This should give COMPILER-ERROR on the default problem 'hello'. +// The console.log is missing a `)`. +// +// @EXPECTED_RESULTS@: COMPILER-ERROR + +import { hello } from './module.js'; +console.log(hello(); diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-2/module.js b/example_problems/hello/submissions/compiler_error/multifile-js-2/module.js new file mode 100644 index 00000000000..94f777f1c90 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-2/module.js @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-2/package.json b/example_problems/hello/submissions/compiler_error/multifile-js-2/package.json new file mode 100644 index 00000000000..6990891ff35 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-2/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-3/hello.js b/example_problems/hello/submissions/compiler_error/multifile-js-3/hello.js new file mode 100644 index 00000000000..901213d7f93 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-3/hello.js @@ -0,0 +1,9 @@ +// This should give CORRECT on the default problem 'hello'. +// The submission includes another file with invalid syntax +// which is never used. As we check all files for syntax we +// also check the unused but invalid other file resulting in +// the (unneeded) error. +// +// @EXPECTED_RESULTS@: COMPILER-ERROR + +console.log("Hello world!"); diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-3/module.js b/example_problems/hello/submissions/compiler_error/multifile-js-3/module.js new file mode 100644 index 00000000000..f88727f2c97 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-3/module.js @@ -0,0 +1,6 @@ +// Extra included file with (invalid) JS code. +// Invalid syntax on import instead of export. +// The file itself is never used in the submission. +import function hello() { + return "Hello world!" +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-3/package.json b/example_problems/hello/submissions/compiler_error/multifile-js-3/package.json new file mode 100644 index 00000000000..6990891ff35 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-3/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js/hello.js b/example_problems/hello/submissions/compiler_error/multifile-js/hello.js new file mode 100644 index 00000000000..9d4cc4d81c8 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js/hello.js @@ -0,0 +1,7 @@ +// This should give COMPILER-ERROR on the default problem 'hello'. +// The included file is invalid syntax. +// +// @EXPECTED_RESULTS@: COMPILER-ERROR + +import { hello } from './module.js'; +console.log(hello()); diff --git a/example_problems/hello/submissions/compiler_error/multifile-js/module.js b/example_problems/hello/submissions/compiler_error/multifile-js/module.js new file mode 100644 index 00000000000..1059b80c1c0 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js/module.js @@ -0,0 +1,6 @@ +// Extra included file with JS code +// See the misspelling of let into letter. +export function hello() { + letter unused = 1 + return "Hello world!" +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js/package.json b/example_problems/hello/submissions/compiler_error/multifile-js/package.json new file mode 100644 index 00000000000..6990891ff35 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/compiler_error/test-syntax-error.R b/example_problems/hello/submissions/compiler_error/test-syntax-error.R new file mode 100644 index 00000000000..dbb00f41591 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/test-syntax-error.R @@ -0,0 +1,5 @@ +# This should give COMPILER-ERROR on the default problem 'hello'. +# +# @EXPECTED_RESULTS@: COMPILER-ERROR + +cat("Missing closing bracket!\n" diff --git a/example_problems/hello/submissions/run_time_error/multifile-js/hello.js b/example_problems/hello/submissions/run_time_error/multifile-js/hello.js new file mode 100644 index 00000000000..efebb3d22c4 --- /dev/null +++ b/example_problems/hello/submissions/run_time_error/multifile-js/hello.js @@ -0,0 +1,8 @@ +// This should give RUN-ERROR on the default problem 'hello'. +// It tries to include another file for the actual implementation +// with the wrong extension. +// +// @EXPECTED_RESULTS@: RUN-ERROR + +import { hello } from './module.mjs'; +console.log(hello()); diff --git a/example_problems/hello/submissions/run_time_error/multifile-js/module.js b/example_problems/hello/submissions/run_time_error/multifile-js/module.js new file mode 100644 index 00000000000..94f777f1c90 --- /dev/null +++ b/example_problems/hello/submissions/run_time_error/multifile-js/module.js @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/run_time_error/multifile-js/package.json b/example_problems/hello/submissions/run_time_error/multifile-js/package.json new file mode 100644 index 00000000000..6990891ff35 --- /dev/null +++ b/example_problems/hello/submissions/run_time_error/multifile-js/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/gitlab/base.sh b/gitlab/base.sh index 00cdfb35762..a07f026d5bc 100755 --- a/gitlab/base.sh +++ b/gitlab/base.sh @@ -48,21 +48,24 @@ parameters: domjudge.webappdir: /webapp domjudge.libdir: /lib domjudge.sqldir: /sql - domjudge.libvendordir: /lib/vendor + domjudge.vendordir: /webapp/vendor domjudge.logdir: /output/log domjudge.rundir: /output/run domjudge.tmpdir: /output/tmp domjudge.baseurl: http://localhost/domjudge EOF +# Composer steps +cd webapp # install check if the cache might be dirty set +e -composer install --no-scripts || rm -rf lib/vendor +composer install --no-scripts || rm -rf vendor set -e # install all php dependencies composer install --no-scripts echo -e "\033[0m" +cd $DIR # configure, make and install (but skip documentation) make configure diff --git a/gitlab/ci/integration.yml b/gitlab/ci/integration.yml index 6996ceca180..d4152ab6874 100644 --- a/gitlab/ci/integration.yml +++ b/gitlab/ci/integration.yml @@ -3,9 +3,9 @@ stage: integration script: - set -eux - - if [ -z ${PHPVERSION+x} ]; then export PHPVERSION=8.1; fi + - if [ -z ${PHPVERSION+x} ]; then export PHPVERSION=8.3; fi - if [ "$TEST" = "E2E" ]; then exit 0; fi - - if [ "$CRAWL_DATASOURCES" != "0" ]; then exit 0; fi + - if [ "$CRAWL_SHADOW_MODE" != "0" ]; then exit 0; fi - timeout --signal=15 40m ./gitlab/integration.sh $PHPVERSION artifacts: when: always @@ -29,7 +29,7 @@ integration_mysql: MYSQL_REQUIRE_PRIMARY_KEY: 1 PIN_JUDGEDAEMON: 1 TEST: "Unit" - CRAWL_DATASOURCES: "0" + CRAWL_SHADOW_MODE: "0" integration_mariadb_pr: except: @@ -61,4 +61,4 @@ integration_unpinned_judgehost: MARIADB_PORT_3306_TCP_ADDR: sqlserver PIN_JUDGEDAEMON: 0 TEST: "Unit" - CRAWL_DATASOURCES: "0" + CRAWL_SHADOW_MODE: "0" diff --git a/gitlab/ci/template.yml b/gitlab/ci/template.yml index 902599d6e75..86521a2f22b 100644 --- a/gitlab/ci/template.yml +++ b/gitlab/ci/template.yml @@ -27,16 +27,15 @@ .cached_vendor: extends: [.clean_ordering] cache: - key: libvendor-260522 + key: webappvendor-20240623 paths: - - lib/vendor/ + - webapp/vendor/ .mysql_job: script: - /bin/true services: - name: mysql - command: ["--default-authentication-plugin=mysql_native_password"] alias: sqlserver .mariadb_job: @@ -51,15 +50,15 @@ - /bin/true parallel: matrix: - - PHPVERSION: ["8.1","8.2"] + - PHPVERSION: ["8.1","8.2","8.3"] TEST: ["E2E","Unit"] - CRAWL_DATASOURCES: ["0","1","2"] + CRAWL_SHADOW_MODE: ["0","1"] .phpsupported_job_pr: script: - /bin/true parallel: matrix: - - PHPVERSION: ["8.2"] + - PHPVERSION: ["8.3"] TEST: ["E2E","Unit"] - CRAWL_DATASOURCES: ["0"] + CRAWL_SHADOW_MODE: ["0"] diff --git a/gitlab/ci/unit.yml b/gitlab/ci/unit.yml index 1d0eea3d70f..d9a91557b1a 100644 --- a/gitlab/ci/unit.yml +++ b/gitlab/ci/unit.yml @@ -12,9 +12,9 @@ - set -eux - if [ -z ${PHPVERSION+x} ]; then export PHPVERSION=8.1; fi - if [ -z ${TEST+x} ]; then export TEST="UNIT"; fi - - if [ "$TEST" = "UNIT" ] && [ "$CRAWL_DATASOURCES" != "0" ]; then exit 0; fi - - if [ "$TEST" = "E2E" ] && [ "$CRAWL_DATASOURCES" != "0" ] && [ "$CI_COMMIT_BRANCH" != "main" ]; then exit 0; fi - - export CRAWL_DATASOURCES + - if [ "$TEST" = "UNIT" ] && [ "$CRAWL_SHADOW_MODE" != "0" ]; then exit 0; fi + - if [ "$TEST" = "E2E" ] && [ "$CRAWL_SHADOW_MODE" != "0" ] && [ "$CI_COMMIT_BRANCH" != "main" ]; then exit 0; fi + - export CRAWL_SHADOW_MODE - ./gitlab/unit-tests.sh $PHPVERSION $TEST artifacts: when: always @@ -48,4 +48,4 @@ run unit tests (MySQL): parallel: matrix: - TEST: ["E2E","Unit"] - CRAWL_DATASOURCES: ["0"] + CRAWL_SHADOW_MODE: ["0"] diff --git a/gitlab/integration.sh b/gitlab/integration.sh index 2ee36099ec8..a4678e19ea3 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 @@ -129,19 +143,23 @@ if [ $PIN_JUDGEDAEMON -eq 1 ]; then fi section_end more_setup -section_start runguard_tests "Running isolated runguard tests" -sudo addgroup domjudge-run-0 -sudo usermod -g domjudge-run-0 domjudge-run-0 -cd ${DIR}/judge/runguard_test -make test -section_end runguard_tests +if [ $cgroupv1 -ne 0 ]; then + section_start runguard_tests "Running isolated runguard tests" + sudo addgroup domjudge-run-0 + sudo usermod -g domjudge-run-0 domjudge-run-0 + cd ${DIR}/judge/runguard_test + make test + section_end runguard_tests +fi -section_start start_judging "Start judging" -cd /opt/domjudge/judgehost/ +if [ $cgroupv1 -ne 0 ]; then + section_start start_judging "Start judging" + cd /opt/domjudge/judgehost/ -sudo -u domjudge bin/judgedaemon $PINNING |& tee /tmp/judgedaemon.log & -sleep 5 -section_end start_judging + sudo -u domjudge bin/judgedaemon $PINNING |& tee /tmp/judgedaemon.log & + sleep 5 + section_end start_judging +fi section_start submitting "Importing Kattis examples" export SUBMITBASEURL='http://localhost/domjudge/' @@ -160,13 +178,11 @@ for i in hello_kattis different guess; do cd "$i" zip -r "../${i}.zip" -- * ) - curl --fail -X POST -n -N -F zip=@${i}.zip http://localhost/domjudge/api/contests/1/problems + curl --fail -X POST -n -N -F zip=@${i}.zip http://localhost/domjudge/api/contests/demo/problems 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" @@ -180,56 +196,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/demo/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, + # - one submissions submitted through the submit client, and thus the magic string ignored, + # - and all submissions to be judged. + if [ $NUMNOTVERIFIED -ne 2 ] || [ $NUMNOMAGIC -ne 1 ] || [ $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 1)." + 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,8 +263,8 @@ set -x # Finalize contest so that awards appear in the feed; first freeze and end the # contest if that has not already been done. export CURLOPTS="--fail -m 30 -b $COOKIEJAR" -curl $CURLOPTS -X POST -d 'contest=1&donow[freeze]=freeze now' http://localhost/domjudge/jury/contests || true -curl $CURLOPTS -X POST -d 'contest=1&donow[end]=end now' http://localhost/domjudge/jury/contests || true +curl $CURLOPTS http://localhost/domjudge/jury/contests/1/freeze/doNow || true +curl $CURLOPTS http://localhost/domjudge/jury/contests/1/end/doNow || true curl $CURLOPTS -X POST -d 'finalize_contest[b]=0&finalize_contest[finalizecomment]=gitlab&finalize_contest[finalize]=' http://localhost/domjudge/jury/contests/1/finalize # shellcheck disable=SC2002,SC2196 @@ -249,7 +273,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/gitlab/unit-tests.sh b/gitlab/unit-tests.sh index 579af80f1d9..63ae58121e1 100755 --- a/gitlab/unit-tests.sh +++ b/gitlab/unit-tests.sh @@ -20,7 +20,7 @@ echo "UPDATE user SET teamid = 1 WHERE userid = 1;" | mysql domjudge_test cp webapp/.env.test /opt/domjudge/domserver/webapp/ # We also need the composer.json for PHPunit to detect the correct directory. -cp composer.json /opt/domjudge/domserver/ +cp webapp/composer.json /opt/domjudge/domserver/webapp/ cd /opt/domjudge/domserver @@ -40,7 +40,7 @@ if [ $CODECOVERAGE -eq 1 ]; then CNT=$(sed -n '/Generating code coverage report/,$p' "$GITLABARTIFACTS"/phpunit.out | grep -v DoctrineTestBundle | grep -cv ^$) FILE=deprecation.txt sed -n '/Generating code coverage report/,$p' "$GITLABARTIFACTS"/phpunit.out > ${CI_PROJECT_DIR}/$FILE - if [ $CNT -le 74 ]; then + if [ $CNT -le 32 ]; then STATE=success else STATE=failure @@ -59,6 +59,8 @@ if [ $UNITSUCCESS -eq 0 ]; then else STATE=failure fi +cp webapp/var/log/test.log "$GITLABARTIFACTS"/test.log + curl https://api.github.com/repos/domjudge/domjudge/statuses/$CI_COMMIT_SHA \ -X POST \ -H "Authorization: token $GH_BOT_TOKEN_OBSCURED" \ diff --git a/gitlab/webstandard.sh b/gitlab/webstandard.sh deleted file mode 100755 index 19fc925f92d..00000000000 --- a/gitlab/webstandard.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash - -. gitlab/ci_settings.sh - -section_start_collap setup "Setup and install" - -export version=8.1 - -show_phpinfo $version - -# Set up -"$( dirname "${BASH_SOURCE[0]}" )"/base.sh - -trap log_on_err ERR - -cd /opt/domjudge/domserver - -section_end setup - -section_start_collap testuser "Setup the test user" -# We're using the admin user in all possible roles -echo "DELETE FROM userrole WHERE userid=1;" | mysql domjudge -ADMINPASS=$(cat etc/initial_admin_password.secret) -export COOKIEJAR -COOKIEJAR=$(mktemp --tmpdir) -export CURLOPTS="--fail -sq -m 30 -b $COOKIEJAR" -if [ $ROLE = "public" ]; then - ADMINPASS="failedlogin" -elif [ $ROLE = "team" ]; then - # Add team to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 3);" | mysql domjudge - echo "UPDATE user SET teamid = 1 WHERE userid = 1;" | mysql domjudge -elif [ $ROLE = "jury" ]; then - # Add jury to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 2);" | mysql domjudge -elif [ $ROLE = "balloon" ]; then - # Add balloon to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 4);" | mysql domjudge -elif [ $ROLE = "admin" ]; then - # Add admin to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 1);" | mysql domjudge -fi - -# Make an initial request which will get us a session id, and grab the csrf token from it -CSRFTOKEN=$(curl $CURLOPTS -c $COOKIEJAR "http://localhost/domjudge/login" 2>/dev/null | sed -n 's/.*_csrf_token.*value="\(.*\)".*/\1/p') -# Make a second request with our session + csrf token to actually log in -curl $CURLOPTS -c $COOKIEJAR -F "_csrf_token=$CSRFTOKEN" -F "_username=admin" -F "_password=$ADMINPASS" "http://localhost/domjudge/login" - -cd $DIR - -cp $COOKIEJAR cookies.txt -sed -i 's/#HttpOnly_//g' cookies.txt -sed -i 's/\t0\t/\t1999999999\t/g' cookies.txt -section_end testuser - -# Could try different entrypoints -FOUNDERR=0 -URL=public -mkdir $URL -cd $URL -cp $DIR/cookies.txt ./ -section_start_collap scrape "Scrape the site with the rebuild admin user" -set +e -wget \ --reject-regex logout \ --recursive \ --no-clobber \ --page-requisites \ --html-extension \ --convert-links \ --restrict-file-names=windows \ --domains localhost \ --no-parent \ --load-cookies cookies.txt \ http://localhost/domjudge/$URL -RET=$? -set -e -#https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html -# Exit code 4 is network error which we can ignore -if [ $RET -ne 4 ] && [ $RET -ne 0 ]; then - exit $RET -fi -section_end scrape - -if [ "$TEST" = "w3cval" ]; then - section_start_collap upstream_problems "Remove files from upstream with problems" - rm -rf localhost/domjudge/doc - rm -rf localhost/domjudge/css/fontawesome-all.min.css* - rm -rf localhost/domjudge/bundles/nelmioapidoc* - rm -f localhost/domjudge/css/bootstrap.min.css* - rm -f localhost/domjudge/css/select2-bootstrap*.css* - rm -f localhost/domjudge/css/dataTables*.css* - rm -f localhost/domjudge/jury/config/check/phpinfo* - section_end upstream_problems - - section_start_collap test_suite "Install testsuite" - cd $DIR - wget https://github.com/validator/validator/releases/latest/download/vnu.linux.zip - unzip -q vnu.linux.zip - section_end test_suite - FLTR='--filterpattern .*autocomplete.*|.*style.*' - for typ in html css svg - do - $DIR/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format json $FLTR $URL 2> result.json - NEWFOUNDERRORS=`$DIR/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format gnu $FLTR $URL 2>&1 | wc -l` - FOUNDERR=$((NEWFOUNDERRORS+FOUNDERR)) - python3 -m "json.tool" < result.json > w3c$typ$URL.json - trace_off; python3 gitlab/jsontogitlab.py w3c$typ$URL.json; trace_on - done -else - section_start_collap upstream_problems "Remove files from upstream with problems" - rm -rf localhost/domjudge/{doc,api} - section_end upstream_problems - - if [ $TEST == "axe" ]; then - STAN="-e $TEST" - FLTR="" - else - STAN="-s $TEST" - FLTR="-E '#DataTables_Table_0 > tbody > tr > td > a','#menuDefault > a','#filter-card > div > div > div > span > span:nth-child(1) > span > ul > li > input','.problem-badge'" - fi - cd $DIR - ACCEPTEDERR=5 - # shellcheck disable=SC2044,SC2035 - for file in `find $URL -name *.html` - do - section_start ${file//\//} $file - # T is reasonable amount of errors to allow to not break - su domjudge -c "/node_modules/.bin/pa11y $STAN -T $ACCEPTEDERR $FLTR --reporter json ./$file" | python3 -m json.tool - ERR=`su domjudge -c "/node_modules/.bin/pa11y $STAN -T $ACCEPTEDERR $FLTR --reporter csv ./$file" | wc -l` - FOUNDERR=$((ERR+FOUNDERR-1)) # Remove header row - section_end $file - done -fi -echo "Found: " $FOUNDERR -[ "$FOUNDERR" -eq 0 ] diff --git a/judge/Makefile b/judge/Makefile index f938dc69306..b206de0409b 100644 --- a/judge/Makefile +++ b/judge/Makefile @@ -13,14 +13,12 @@ judgehost: $(TARGETS) $(SUBST_FILES) $(SUBST_FILES): %: %.in $(TOPDIR)/paths.mk $(substconfigvars) -runguard: LDFLAGS := $(filter-out -pie,$(LDFLAGS)) - -runguard: -lm $(LIBCGROUP) -runguard$(OBJEXT): $(TOPDIR)/etc/runguard-config.h - evict: evict.c $(LIBHEADERS) $(LIBSOURCES) $(CC) $(CFLAGS) -o $@ $< $(LIBSOURCES) +runguard: runguard.cc $(LIBHEADERS) $(LIBSOURCES) $(TOPDIR)/etc/runguard-config.h + $(CXX) $(CXXFLAGS) -o $@ $< $(LIBSOURCES) $(LIBCGROUP) + runpipe: runpipe.cc $(LIBHEADERS) $(LIBSOURCES) $(CXX) $(CXXFLAGS) -static -o $@ $< $(LIBSOURCES) @@ -32,11 +30,6 @@ install-judgehost: judgedaemon.main.php run-interactive.sh $(INSTALL_PROG) -t $(DESTDIR)$(judgehost_bindir) \ judgedaemon runguard runpipe create_cgroups -ifneq ($(systemd_unitdir),) - $(INSTALL_DIR) $(DESTDIR)$(systemd_unitdir) - $(INSTALL_DATA) -t $(DESTDIR)$(systemd_unitdir) \ - create-cgroups.service domjudge-judgehost.target domjudge-judgedaemon@.service -endif clean-l: -rm -f $(TARGETS) $(TARGETS:%=%$(OBJEXT)) diff --git a/judge/create_cgroups.in b/judge/create_cgroups.in index fbfe48898be..56d1338a31c 100755 --- a/judge/create_cgroups.in +++ b/judge/create_cgroups.in @@ -7,7 +7,7 @@ # (hence: not the 'domjudge-run' user!) JUDGEHOSTUSER=@DOMJUDGE_USER@ -CGROUPBASE=@judgehost_cgroupdir@ +CGROUPBASE="/sys/fs/cgroup" cgroup_error_and_usage () { echo "$1" >&2 diff --git a/judge/domjudge-judgedaemon@.service.in b/judge/domjudge-judgedaemon@.service.in index 08acade799d..044a3213334 100644 --- a/judge/domjudge-judgedaemon@.service.in +++ b/judge/domjudge-judgedaemon@.service.in @@ -20,6 +20,9 @@ Type=simple ExecStart=@judgehost_bindir@/judgedaemon -n %i User=@DOMJUDGE_USER@ +KillSignal=SIGTERM +TimeoutStopSec=180 +FinalKillSignal=SIGKILL Restart=always RestartSec=3 diff --git a/judge/runguard.c b/judge/runguard.cc similarity index 84% rename from judge/runguard.c rename to judge/runguard.cc index 443a28b6247..a63e65e4aeb 100644 --- a/judge/runguard.c +++ b/judge/runguard.cc @@ -32,11 +32,6 @@ /* Some system/site specific config: VALID_USERS, CHROOT_PREFIX */ #include "runguard-config.h" -/* For chroot(), which is not POSIX. */ -#define _DEFAULT_SOURCE -/* For unshare() used by cgroups. */ -#define _GNU_SOURCE - #include #include #include @@ -46,23 +41,23 @@ #include #include #include -#include +#include #include -#include -#include +#include +#include #include -#include -#include -#include +#include +#include +#include #include #include #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include @@ -100,10 +95,6 @@ const struct timespec cg_delete_delay = { 0, 10000000L }; /* 0.01 seconds */ extern int errno; -#ifndef _GNU_SOURCE -extern char **environ; -#endif - const int exit_failure = -1; char *progname; @@ -145,6 +136,7 @@ int be_verbose; int be_quiet; int show_help; int show_version; +pid_t runpipe_pid = -1; double walltimelimit[2], cputimelimit[2]; /* in seconds, soft and hard limits */ int walllimit_reached, cpulimit_reached; /* 1=soft, 2=hard, 3=both limits reached */ @@ -159,8 +151,6 @@ pid_t child_pid = -1; static volatile sig_atomic_t received_SIGCHLD = 0; static volatile sig_atomic_t received_signal = -1; -FILE *child_stdout; -FILE *child_stderr; int child_pipefd[3][2]; int child_redirfd[3]; @@ -168,28 +158,29 @@ struct timeval progstarttime, starttime, endtime; struct tms startticks, endticks; struct option const long_opts[] = { - {"root", required_argument, NULL, 'r'}, - {"user", required_argument, NULL, 'u'}, - {"group", required_argument, NULL, 'g'}, - {"chdir", required_argument, NULL, 'd'}, - {"walltime", required_argument, NULL, 't'}, - {"cputime", required_argument, NULL, 'C'}, - {"memsize", required_argument, NULL, 'm'}, - {"filesize", required_argument, NULL, 'f'}, - {"nproc", required_argument, NULL, 'p'}, - {"cpuset", required_argument, NULL, 'P'}, - {"no-core", no_argument, NULL, 'c'}, - {"stdout", required_argument, NULL, 'o'}, - {"stderr", required_argument, NULL, 'e'}, - {"streamsize", required_argument, NULL, 's'}, - {"environment",no_argument, NULL, 'E'}, - {"variable", required_argument, NULL, 'V'}, - {"outmeta", required_argument, NULL, 'M'}, - {"verbose", no_argument, NULL, 'v'}, - {"quiet", no_argument, NULL, 'q'}, - {"help", no_argument, &show_help, 1 }, - {"version", no_argument, &show_version, 1 }, - { NULL, 0, NULL, 0 } + {"root", required_argument, nullptr, 'r'}, + {"user", required_argument, nullptr, 'u'}, + {"group", required_argument, nullptr, 'g'}, + {"chdir", required_argument, nullptr, 'd'}, + {"walltime", required_argument, nullptr, 't'}, + {"cputime", required_argument, nullptr, 'C'}, + {"memsize", required_argument, nullptr, 'm'}, + {"filesize", required_argument, nullptr, 'f'}, + {"nproc", required_argument, nullptr, 'p'}, + {"cpuset", required_argument, nullptr, 'P'}, + {"no-core", no_argument, nullptr, 'c'}, + {"stdout", required_argument, nullptr, 'o'}, + {"stderr", required_argument, nullptr, 'e'}, + {"streamsize", required_argument, nullptr, 's'}, + {"environment",no_argument, nullptr, 'E'}, + {"variable", required_argument, nullptr, 'V'}, + {"outmeta", required_argument, nullptr, 'M'}, + {"runpipepid", required_argument, nullptr, 'U'}, + {"verbose", no_argument, nullptr, 'v'}, + {"quiet", no_argument, nullptr, 'q'}, + {"help", no_argument, &show_help, 1 }, + {"version", no_argument, &show_version, 1 }, + { nullptr, 0, nullptr, 0 } }; void warning( const char *, ...) __attribute__((format (printf, 1, 2))); @@ -215,13 +206,12 @@ void verbose(const char *format, ...) { va_list ap; va_start(ap,format); - struct timeval currtime; - double runtime; if ( ! be_quiet && be_verbose ) { - gettimeofday(&currtime,NULL); - runtime = (currtime.tv_sec - progstarttime.tv_sec ) + - (currtime.tv_usec - progstarttime.tv_usec)*1E-6; + struct timeval currtime{}; + gettimeofday(&currtime,nullptr); + double runtime = (currtime.tv_sec - progstarttime.tv_sec ) + + (currtime.tv_usec - progstarttime.tv_usec)*1E-6; fprintf(stderr,"%s [%d @ %10.6lf]: verbose: ",progname,getpid(),runtime); vfprintf(stderr,format,ap); fprintf(stderr,"\n"); @@ -234,29 +224,27 @@ void error(int errnum, const char *format, ...) { va_list ap; va_start(ap,format); - sigset_t sigs; - char *errstr; - int errlen, errpos; /* * Make sure the signal handler for these (terminate()) does not * interfere, we are exiting now anyway. */ + sigset_t sigs; sigaddset(&sigs, SIGALRM); sigaddset(&sigs, SIGTERM); - sigprocmask(SIG_BLOCK, &sigs, NULL); + sigprocmask(SIG_BLOCK, &sigs, nullptr); /* First print to string to be able to reuse the message. */ - errlen = strlen(progname)+255; - if ( format!=NULL ) errlen += strlen(format); + size_t errlen = strlen(progname)+255; + if ( format!=nullptr ) errlen += strlen(format); - errstr = (char *)malloc(errlen); - if ( errstr==NULL ) abort(); + char *errstr = (char *)malloc(errlen); + if ( errstr==nullptr ) abort(); sprintf(errstr,"%s",progname); - errpos = strlen(errstr); + size_t errpos = strlen(errstr); - if ( format!=NULL ) { + if ( format!=nullptr ) { snprintf(errstr+errpos,errlen-errpos,": "); errpos += 2; vsnprintf(errstr+errpos,errlen-errpos,format,ap); @@ -276,7 +264,7 @@ void error(int errnum, const char *format, ...) } errpos += strlen(errstr+errpos); } - if ( format==NULL && errnum==0 ) { + if ( format==nullptr && errnum==0 ) { snprintf(errstr+errpos,errlen-errpos,": unknown error"); } @@ -284,7 +272,7 @@ void error(int errnum, const char *format, ...) va_end(ap); write_meta("internal-error","%s",errstr); - if ( outputmeta && metafile != NULL && fclose(metafile)!=0 ) { + if ( outputmeta && metafile != nullptr && fclose(metafile)!=0 ) { fprintf(stderr,"\nError writing to metafile '%s'.\n",metafilename); } @@ -303,7 +291,7 @@ void error(int errnum, const char *format, ...) } /* Wait a while to make sure the process is killed by now. */ - nanosleep(&killdelay,NULL); + nanosleep(&killdelay,nullptr); } exit(exit_failure); @@ -311,10 +299,9 @@ void error(int errnum, const char *format, ...) void write_meta(const char *key, const char *format, ...) { - va_list ap; - if ( !outputmeta ) return; + va_list ap; va_start(ap,format); if ( fprintf(metafile,"%s: ",key)<=0 ) { @@ -364,8 +351,11 @@ Run COMMAND with restrictions.\n\ -e, --stderr=FILE redirect COMMAND stderr output to FILE\n\ -s, --streamsize=SIZE truncate COMMAND stdout/stderr streams at SIZE kB\n\ -E, --environment preserve environment variables (default only PATH)\n\ - -V, --variable add additional environment variables (in form KEY=VALUE;KEY2=VALUE2)\n\ - -M, --outmeta=FILE write metadata (runtime, exitcode, etc.) to FILE\n"); + -V, --variable add additional environment variables\n\ + (in form KEY=VALUE;KEY2=VALUE2)\n\ + -M, --outmeta=FILE write metadata (runtime, exitcode, etc.) to FILE\n\ + -U, --runpipepid=PID process ID of runpipe to send SIGUSR1 signal when\n\ + timelimit is reached\n"); printf("\ -v, --verbose display some extra warnings and information\n\ -q, --quiet suppress all warnings and verbose output\n\ @@ -386,10 +376,6 @@ real user ID.\n"); void output_exit_time(int exitcode, double cpudiff) { - double walldiff, userdiff, sysdiff; - int timelimit_reached = 0; - unsigned long ticks_per_second = sysconf(_SC_CLK_TCK); - verbose("command exited with exitcode %d",exitcode); write_meta("exitcode","%d",exitcode); @@ -397,11 +383,12 @@ void output_exit_time(int exitcode, double cpudiff) write_meta("signal", "%d", received_signal); } - walldiff = (endtime.tv_sec - starttime.tv_sec ) + - (endtime.tv_usec - starttime.tv_usec)*1E-6; + double walldiff = (endtime.tv_sec - starttime.tv_sec ) + + (endtime.tv_usec - starttime.tv_usec)*1E-6; - userdiff = (double)(endticks.tms_cutime - startticks.tms_cutime) / ticks_per_second; - sysdiff = (double)(endticks.tms_cstime - startticks.tms_cstime) / ticks_per_second; + unsigned long ticks_per_second = sysconf(_SC_CLK_TCK); + double userdiff = (double)(endticks.tms_cutime - startticks.tms_cutime) / ticks_per_second; + double sysdiff = (double)(endticks.tms_cstime - startticks.tms_cstime) / ticks_per_second; write_meta("wall-time","%.3f", walldiff); write_meta("user-time","%.3f", userdiff); @@ -421,6 +408,7 @@ void output_exit_time(int exitcode, double cpudiff) warning("timelimit exceeded (soft cpu time)"); } + int timelimit_reached = 0; switch ( outputtimetype ) { case WALL_TIME_TYPE: write_meta("time-used","wall-time"); @@ -444,31 +432,31 @@ void output_exit_time(int exitcode, double cpudiff) void check_remaining_procs() { - char path[1024]; - - snprintf(path, 1023, "/sys/fs/cgroup/cpuacct%scgroup.procs", cgroupname); - FILE *file = fopen(path, "r"); - if (file == NULL) { - error(errno, "opening cgroups file `%s'", path); - } - - fseek(file, 0L, SEEK_END); - if (ftell(file) > 0) { - error(0, "found left-over processes in cgroup controller, please check!"); - } + char path[1024]; + snprintf(path, 1023, "/sys/fs/cgroup/cpuacct%scgroup.procs", cgroupname); + + FILE *file = fopen(path, "r"); + if (file == nullptr) { + error(errno, "opening cgroups file `%s'", path); + } + + fseek(file, 0L, SEEK_END); + if (ftell(file) > 0) { + error(0, "found left-over processes in cgroup controller, please check!"); + } if (fclose(file) != 0) error(errno, "closing file `%s'", path); } void output_cgroup_stats(double *cputime) { - int ret; - int64_t max_usage, cpu_time_int; struct cgroup *cg; - struct cgroup_controller *cg_controller; + if ( (cg = cgroup_new_cgroup(cgroupname))==nullptr ) error(0,"cgroup_new_cgroup"); - if ( (cg = cgroup_new_cgroup(cgroupname))==NULL ) error(0,"cgroup_new_cgroup"); + int ret; if ((ret = cgroup_get_cgroup(cg)) != 0) error(ret,"get cgroup information"); + int64_t max_usage; + struct cgroup_controller *cg_controller; cg_controller = cgroup_get_controller(cg, "memory"); ret = cgroup_get_value_int64(cg_controller, "memory.memsw.max_usage_in_bytes", &max_usage); if ( ret!=0 ) error(ret,"get cgroup value memory.memsw.max_usage_in_bytes"); @@ -476,6 +464,7 @@ void output_cgroup_stats(double *cputime) verbose("total memory used: %" PRId64 " kB", max_usage/1024); write_meta("memory-bytes","%" PRId64, max_usage); + int64_t cpu_time_int; cg_controller = cgroup_get_controller(cg, "cpuacct"); ret = cgroup_get_value_int64(cg_controller, "cpuacct.usage", &cpu_time_int); if ( ret!=0 ) error(ret,"get cgroup value cpuacct.usage"); @@ -492,27 +481,26 @@ void output_cgroup_stats(double *cputime) void cgroup_create() { - int ret; struct cgroup *cg; - struct cgroup_controller *cg_controller; - cg = cgroup_new_cgroup(cgroupname); if (!cg) error(0,"cgroup_new_cgroup"); /* Set up the memory restrictions; these two options limit ram use and ram+swap use. They are the same so no swapping can occur */ - if ( (cg_controller = cgroup_add_controller(cg, "memory"))==NULL ) { + struct cgroup_controller *cg_controller; + if ( (cg_controller = cgroup_add_controller(cg, "memory"))==nullptr ) { error(0,"cgroup_add_controller memory"); } + int ret; cgroup_add_value(int64, "memory.limit_in_bytes", memsize); cgroup_add_value(int64, "memory.memsw.limit_in_bytes", memsize); /* Set up cpu restrictions; we pin the task to a specific set of cpus. We also give it exclusive access to those cores, and set no limits on memory nodes */ - if ( cpuset!=NULL && strlen(cpuset)>0 ) { - if ( (cg_controller = cgroup_add_controller(cg, "cpuset"))==NULL ) { + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { + if ( (cg_controller = cgroup_add_controller(cg, "cpuset"))==nullptr ) { error(0,"cgroup_add_controller cpuset"); } /* To make a cpuset exclusive, some additional setup outside of domjudge is @@ -524,7 +512,7 @@ void cgroup_create() verbose("cpuset undefined"); } - if ( (cg_controller = cgroup_add_controller(cg, "cpuacct"))==NULL ) { + if ( (cg_controller = cgroup_add_controller(cg, "cpuacct"))==nullptr ) { error(0,"cgroup_add_controller cpuacct"); } @@ -539,12 +527,11 @@ void cgroup_create() void cgroup_attach() { - int ret; struct cgroup *cg; - cg = cgroup_new_cgroup(cgroupname); if (!cg) error(0,"cgroup_new_cgroup"); + int ret; if ( (ret = cgroup_get_cgroup(cg))!=0 ) error(ret,"get cgroup information"); /* Attach task to the cgroup */ @@ -555,13 +542,12 @@ void cgroup_attach() void cgroup_kill() { - int ret; - void *handle = NULL; + void *handle = nullptr; pid_t pid; /* kill any remaining tasks, and wait for them to be gone */ while(1) { - ret = cgroup_get_task_begin(cgroupname, "memory", &handle, &pid); + int ret = cgroup_get_task_begin(cgroupname, "memory", &handle, &pid); cgroup_get_task_end(&handle); if (ret == ECGEOF) break; kill(pid, SIGKILL); @@ -570,21 +556,19 @@ void cgroup_kill() void cgroup_delete() { - int ret; struct cgroup *cg; - cg = cgroup_new_cgroup(cgroupname); if (!cg) error(0,"cgroup_new_cgroup"); - if ( cgroup_add_controller(cg, "cpuacct")==NULL ) error(0,"cgroup_add_controller cpuacct"); - if ( cgroup_add_controller(cg, "memory")==NULL ) error(0,"cgroup_add_controller memory"); + if ( cgroup_add_controller(cg, "cpuacct")==nullptr ) error(0,"cgroup_add_controller cpuacct"); + if ( cgroup_add_controller(cg, "memory")==nullptr ) error(0,"cgroup_add_controller memory"); - if ( cpuset!=NULL && strlen(cpuset)>0 ) { - if ( cgroup_add_controller(cg, "cpuset")==NULL ) error(0,"cgroup_add_controller cpuset"); + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { + if ( cgroup_add_controller(cg, "cpuset")==nullptr ) error(0,"cgroup_add_controller cpuset"); } /* Clean up our cgroup */ - nanosleep(&cg_delete_delay,NULL); - ret = cgroup_delete_cgroup_ext(cg, CGFLAG_DELETE_IGNORE_MIGRATION | CGFLAG_DELETE_RECURSIVE); + nanosleep(&cg_delete_delay,nullptr); + int ret = cgroup_delete_cgroup_ext(cg, CGFLAG_DELETE_IGNORE_MIGRATION | CGFLAG_DELETE_RECURSIVE); if ( ret!=0 ) error(ret,"deleting cgroup"); cgroup_free(&cg); @@ -602,14 +586,19 @@ void terminate(int sig) if ( sigemptyset(&sigact.sa_mask)!=0 ) { warning("could not initialize signal mask"); } - if ( sigaction(SIGTERM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGTERM,&sigact,nullptr)!=0 ) { warning("could not restore signal handler"); } - if ( sigaction(SIGALRM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGALRM,&sigact,nullptr)!=0 ) { warning("could not restore signal handler"); } if ( sig==SIGALRM ) { + if (runpipe_pid > 0) { + warning("sending SIGUSR1 to runpipe with pid %d", runpipe_pid); + kill(runpipe_pid, SIGUSR1); + } + walllimit_reached |= hard_timelimit; warning("timelimit exceeded (hard wall time): aborting command"); } else { @@ -627,7 +616,7 @@ void terminate(int sig) /* Prefer nanosleep over sleep because of higher resolution and it does not interfere with signals. */ - nanosleep(&killdelay,NULL); + nanosleep(&killdelay,nullptr); verbose("sending SIGKILL"); if ( kill(-child_pid,SIGKILL)!=0 && errno!=ESRCH ) { @@ -635,7 +624,7 @@ void terminate(int sig) } /* Wait another while to make sure the process is killed by now. */ - nanosleep(&killdelay,NULL); + nanosleep(&killdelay,nullptr); } static void child_handler(int sig) @@ -645,12 +634,12 @@ static void child_handler(int sig) int userid(char *name) { - struct passwd *pwd; - errno = 0; /* per the linux GETPWNAM(3) man-page */ + + struct passwd *pwd; pwd = getpwnam(name); - if ( pwd==NULL || errno ) return -1; + if ( pwd==nullptr || errno ) return -1; return (int) pwd->pw_uid; } @@ -662,17 +651,17 @@ int groupid(char *name) errno = 0; /* per the linux GETGRNAM(3) man-page */ grp = getgrnam(name); - if ( grp==NULL || errno ) return -1; + if ( grp==nullptr || errno ) return -1; return (int) grp->gr_gid; } long read_optarg_int(const char *desc, long minval, long maxval) { - long arg; char *ptr; - arg = strtol(optarg,&ptr,10); + errno = 0; + long arg = strtol(optarg,&ptr,10); if ( errno || *ptr!='\0' || argmaxval ) { error(errno,"invalid %s specified: `%s'",desc,optarg); } @@ -682,22 +671,25 @@ long read_optarg_int(const char *desc, long minval, long maxval) void read_optarg_time(const char *desc, double *times) { - char *optcopy, *ptr, *sep; - - if ( (optcopy=strdup(optarg))==NULL ) error(0,"strdup() failed"); + char *optcopy; + if ( (optcopy=strdup(optarg))==nullptr ) error(0,"strdup() failed"); /* Check for soft:hard limit separator and cut string. */ - if ( (sep=strchr(optcopy,':'))!=NULL ) *sep = 0; + char *sep; + if ( (sep=strchr(optcopy,':'))!=nullptr ) *sep = 0; + char *ptr; + errno = 0; times[0] = strtod(optcopy,&ptr); if ( errno || *ptr!='\0' || !finite(times[0]) || times[0]<=0 ) { error(errno,"invalid %s specified: `%s'",desc,optarg); } /* And repeat for hard limit if we found the ':' separator. */ - if ( sep!=NULL ) { + if ( sep!=nullptr ) { + errno = 0; times[1] = strtod(sep+1,&ptr); - if ( errno || *ptr!='\0' || !finite(times[1]) || times[1]<=0 ) { + if ( errno || *(sep+1)=='\0' || *ptr!='\0' || !finite(times[1]) || times[1]<=0 ) { error(errno,"invalid %s specified: `%s'",desc,optarg); } if ( times[1]0 ) { + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { int ret = strtol(cpuset, &ptr, 10); /* check if input is only a single integer */ if ( *ptr == '\0' ) { @@ -1181,7 +1170,7 @@ int main(int argc, char **argv) /* Define the cgroup name that we will use and make sure it will * be unique. Note: group names must have slashes! */ - if ( cpuset!=NULL && strlen(cpuset)>0 ) { + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { strncpy(str, cpuset, 16); } else { str[0] = 0; @@ -1200,10 +1189,11 @@ int main(int argc, char **argv) * processes, and at least older versions of sshd seemed to set * it, leading to processes getting a timelimit instead of memory * exceeded, when running via SSH. */ - fp = NULL; + FILE *fp = nullptr; + char *oom_path; if ( !fp && (fp = fopen(OOM_PATH_NEW,"r+")) ) oom_path = strdup(OOM_PATH_NEW); if ( !fp && (fp = fopen(OOM_PATH_OLD,"r+")) ) oom_path = strdup(OOM_PATH_OLD); - if ( fp!=NULL ) { + if ( fp!=nullptr ) { if ( fscanf(fp,"%d",&ret)!=1 ) error(errno,"cannot read from `%s'",oom_path); if ( ret<0 ) { verbose("resetting `%s' from %d to %d",oom_path,ret,OOM_RESET_VALUE); @@ -1226,7 +1216,7 @@ int main(int argc, char **argv) /* Connect pipes to command (stdin/)stdout/stderr and close * unneeded fd's. Do this after setting restrictions to let * any messages not go to command stderr pipe. */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { if ( dup2(child_pipefd[i][PIPE_IN],i)<0 ) { error(errno,"redirecting child fd %d",i); } @@ -1258,17 +1248,17 @@ int main(int argc, char **argv) verbose("watchdog using user ID `%d'",getuid()); } - if ( gettimeofday(&starttime,NULL) ) error(errno,"getting time"); + if ( gettimeofday(&starttime,nullptr) ) error(errno,"getting time"); /* Close unused file descriptors */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { if ( close(child_pipefd[i][PIPE_IN])!=0 ) { error(errno,"closing pipe for fd %i",i); } } /* Redirect child stdout/stderr to file */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { child_redirfd[i] = i; /* Default: no redirects */ data_read[i] = data_passed[i] = 0; /* Reset data counters */ } @@ -1291,7 +1281,7 @@ int main(int argc, char **argv) /* Construct one-time signal handler to terminate() for TERM and ALRM signals. */ - sigmask = emptymask; + sigset_t sigmask = emptymask; if ( sigaddset(&sigmask,SIGALRM)!=0 || sigaddset(&sigmask,SIGTERM)!=0 ) error(errno,"setting signal mask"); @@ -1300,13 +1290,13 @@ int main(int argc, char **argv) sigact.sa_mask = sigmask; /* Kill child command when we receive SIGTERM */ - if ( sigaction(SIGTERM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGTERM,&sigact,nullptr)!=0 ) { error(errno,"installing signal handler"); } if ( use_walltime ) { /* Kill child when we receive SIGALRM */ - if ( sigaction(SIGALRM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGALRM,&sigact,nullptr)!=0 ) { error(errno,"installing signal handler"); } @@ -1316,7 +1306,7 @@ int main(int argc, char **argv) itimer.it_value.tv_sec = (int) walltimelimit[1]; itimer.it_value.tv_usec = (int)(modf(walltimelimit[1],&tmpd) * 1E6); - if ( setitimer(ITIMER_REAL,&itimer,NULL)!=0 ) { + if ( setitimer(ITIMER_REAL,&itimer,nullptr)!=0 ) { error(errno,"setting timer"); } verbose("setting hard wall-time limit to %.3f seconds",walltimelimit[1]); @@ -1329,27 +1319,29 @@ int main(int argc, char **argv) /* Wait for child data or exit. Initialize status here to quelch clang++ warning about uninitialized value; it is set by the wait() call. */ - status = 0; + int status = 0; /* We start using splice() to copy data from child to parent I/O file descriptors. If that fails (not all I/O source - dest combinations support it), then we revert to using read()/write(). */ use_splice = 1; + fd_set readfds; while ( 1 ) { FD_ZERO(&readfds); - nfds = -1; - for(i=1; i<=2; i++) { + int nfds = -1; + for(int i=1; i<=2; i++) { if ( child_pipefd[i][PIPE_OUT]>=0 ) { FD_SET(child_pipefd[i][PIPE_OUT],&readfds); nfds = max(nfds,child_pipefd[i][PIPE_OUT]); } } - r = pselect(nfds+1, &readfds, NULL, NULL, NULL, &emptymask); + int r = pselect(nfds+1, &readfds, nullptr, NULL, NULL, &emptymask); if ( r==-1 && errno!=EINTR ) error(errno,"waiting for child data"); if ( received_SIGCHLD || received_signal == SIGALRM ) { + pid_t pid; if ( (pid = wait(&status))<0 ) error(errno,"waiting on child"); if ( pid==child_pid ) break; } @@ -1359,10 +1351,10 @@ int main(int argc, char **argv) /* Reset pipe filedescriptors to use blocking I/O. */ FD_ZERO(&readfds); - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { if ( child_pipefd[i][PIPE_OUT]>=0 ) { FD_SET(child_pipefd[i][PIPE_OUT],&readfds); - r = fcntl(child_pipefd[i][PIPE_OUT], F_GETFL); + int r = fcntl(child_pipefd[i][PIPE_OUT], F_GETFL); if (r == -1) { error(errno, "fcntl, getting flags"); } @@ -1379,7 +1371,7 @@ int main(int argc, char **argv) } while ( data_passed[1] + data_passed[2] > total_data ); /* Close the output files */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { ret = close(child_redirfd[i]); if( ret!=0 ) error(errno,"closing output fd %d", i); } @@ -1388,10 +1380,10 @@ int main(int argc, char **argv) error(errno,"getting end clock ticks"); } - if ( gettimeofday(&endtime,NULL) ) error(errno,"getting time"); + if ( gettimeofday(&endtime,nullptr) ) error(errno,"getting time"); /* Test whether command has finished abnormally */ - exitcode = 0; + int exitcode = 0; if ( ! WIFEXITED(status) ) { if ( WIFSIGNALED(status) ) { if ( WTERMSIG(status)==SIGXCPU ) { @@ -1422,7 +1414,7 @@ int main(int argc, char **argv) itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = 0; - if ( setitimer(ITIMER_REAL,&itimer,NULL)!=0 ) { + if ( setitimer(ITIMER_REAL,&itimer,nullptr)!=0 ) { error(errno,"disarming timer"); } } diff --git a/judge/runpipe.cc b/judge/runpipe.cc index 06e44098a68..928be2d6224 100644 --- a/judge/runpipe.cc +++ b/judge/runpipe.cc @@ -39,6 +39,9 @@ // emulate it by sending a message in the SIGCHLD signal handler using an extra // pipe. // +// Additionally, we set up a signal handler for SIGUSR1 which runguard will use +// to indicate when it killed its child process due to a time limit. +// // If the proxy is not enabled (i.e. no traffic capturing), the pipes are setup // like this: // @@ -46,6 +49,7 @@ // stdout -----> stdin // stdin <----- stdout // SIGCHLD -----------------> epoll +// SIGUSR1 -----------------> epoll // // // If the proxy is enabled the pipes are setup like this: @@ -54,6 +58,7 @@ // stdout -----> epoll -----> stdin // stdin <----- epoll <----- stdout // SIGCHLD ----------^ +// SIGUSR1 ----------^ #include "config.h" @@ -234,11 +239,20 @@ struct process_t { void spawn() { fd_t stdio[3] = {stdin_fd, stdout_fd, FDREDIR_NONE}; - vector argv(args.size()); + char pid_buf[12]; + vector argv; for (size_t i = 0; i < args.size(); i++) { - argv[i] = args[i].c_str(); + argv.push_back(args[i].c_str()); + if (i == 1 && cmd == "sudo" && + args[i].find("/runguard") != string::npos) { + // This is a hack, and can be improved significantly after implementing + // https://docs.google.com/document/d/1WZRwdvJUamsczYC7CpP3ZIBU8xG6wNqYqrNJf7osxYs/edit#heading=h.i7kgdnmw8qd7 + argv.push_back("-U"); + sprintf(pid_buf, "%d", getpid()); + argv.push_back(pid_buf); + } } - pid = execute(cmd.c_str(), argv.data(), args.size(), stdio, 0); + pid = execute(cmd.c_str(), argv.data(), argv.size(), stdio, 0); if (pid < 0) { error(errno, "failed to execute command #%ld", index); } @@ -398,9 +412,15 @@ struct state_t { // The PID of the first process that exited. pid_t first_process_exit_id = -1; + // Child indicated TLE. + bool child_indicated_timelimit = false; + // The pipe from which the events about the child exits can be read. The // events are writted by the SIGCHLD handler. fd_t child_exited_pipe = -1; + // The pipe from which the events about the child time limits can be read. The + // events are writted by the SIGUSR1 handler. + fd_t child_timelimit_pipe = -1; // The file descriptor of the epoll. fd_t epoll_fd = -1; @@ -572,7 +592,7 @@ struct state_t { } } - // Install an handler for the SIGCHLD signal. The handler will send a byte to + // Install a handler for the SIGCHLD signal. The handler will send a byte to // a pipe notifying the main loop that a child exited. // This method can be called only once. void install_sigchld_handler() { @@ -597,7 +617,7 @@ struct state_t { signal(SIGCHLD, [](int) { // TODO: Decide whether to keep some logging as the line below. We can't // use logmsg here since that will in turn call syslog which is not safe - // to do in a signal handler (see also `man signl-safety`). + // to do in a signal handler (see also `man signal-safety`). // logmsg(LOG_DEBUG, "caught SIGCHLD signal"); // Notify the main loop that a child exited by sending a message via @@ -611,6 +631,46 @@ struct state_t { child_exited_pipe = read_end; } + // Install a handler for the SIGUSR1 signal. The handler will send a byte to + // a pipe notifying the main loop that the child was terminated due to time limit. + // This method can be called only once. + // TODO: Refactor code to avoid code duplication with install_sigchld_handler. + void install_sigusr1_handler() { + fd_t fds[2]; + if (pipe2(fds, O_CLOEXEC | O_NONBLOCK)) { + error(errno, "creating exit pipes"); + } + + // The lambda below cannot capture anything, otherwise it couldn't be made + // into a function pointer. Therefore the write_end must have a static + // lifetime. + fd_t read_end = fds[0]; + static fd_t write_end = -1; + if (write_end != -1) { + error(0, "install_sigchld_handler can be called only once"); + } + write_end = fds[1]; + + logmsg(LOG_DEBUG, "exit handler will send event using %d -> %d", write_end, + read_end); + + signal(SIGUSR1, [](int) { + // TODO: Decide whether to keep some logging as the line below. We can't + // use logmsg here since that will in turn call syslog which is not safe + // to do in a signal handler (see also `man signal-safety`). + // logmsg(LOG_DEBUG, "caught SIGUSR1 signal"); + + // Notify the main loop that a child was terminated due to time limit by sending a message via + // child_timelimit_pipe. + static char buf[] = {42}; + if (write(write_end, buf, 1) != 1) { + error(errno, "failed to notify child exit"); + } + }); + + child_timelimit_pipe = read_end; + } + // Create the pipes used for the process communication, including the ones for // the proxy, if enabled. void setup_pipes() { @@ -685,6 +745,12 @@ struct state_t { } add_fd(child_exited_pipe); + // Always listen for child timelimit events. + if (child_timelimit_pipe == -1) { + error(0, "SIGUSR1 handler not installed"); + } + add_fd(child_timelimit_pipe); + // Listen for incoming data only when proxy is enabled. if (has_proxy()) { for (auto &proc : processes) { @@ -719,7 +785,8 @@ struct state_t { logmsg(LOG_DEBUG, "child with pid %d exited", pid); - if (first_process_exit_id == -1) { + // Only set the first process if runguard didn't tell us about a TLE. + if (first_process_exit_id == -1 && !child_indicated_timelimit) { first_process_exit_id = pid; } @@ -777,7 +844,7 @@ struct state_t { int time_millis = time % 1000; char direction = from.index == 0 ? ']' : '['; char eofbuf[128]; - sprintf(eofbuf, "[%3d.%03ds/%ld]%c", time_sec, time_millis, 0LL, direction); + sprintf(eofbuf, "[%3d.%03ds/%ld]%c", time_sec, time_millis, 0L, direction); write_all(output_file.output_file, eofbuf, strlen(eofbuf)); warning(0, "EOF from process #%ld", from.index); @@ -846,6 +913,19 @@ struct state_t { } continue; } + if (fd == child_timelimit_pipe) { + static char buffer[1]; + if (read(child_timelimit_pipe, buffer, 1) != 1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + error(errno, "failed to read from tle pipe"); + } + } else if (buffer[0] == 42) { + logmsg(LOG_WARNING, "child indicated TLE"); + child_indicated_timelimit = true; + continue; + } + } + // A process wrote in one of the pipes to the proxy. for (size_t i = 0; i < processes.size(); i++) { @@ -904,6 +984,7 @@ int main(int argc, char **argv) { signal(SIGPIPE, SIG_IGN); state.install_sigterm_handler(); state.install_sigchld_handler(); + state.install_sigusr1_handler(); state.setup_pipes(); for (auto &proc : state.processes) { proc.spawn(); diff --git a/judge/testcase_run.sh b/judge/testcase_run.sh index 0f83e1f7ecd..64cdfa51575 100755 --- a/judge/testcase_run.sh +++ b/judge/testcase_run.sh @@ -212,6 +212,12 @@ runcheck "$RUN_SCRIPT" $RUNARGS \ --stderr=program.err --outmeta=program.meta -- \ "$PREFIX/$PROGRAM" 2>runguard.err +if [ "$CREATE_WRITABLE_TEMP_DIR" ]; then + # Revoke access to the TMPDIR as security measure + chown -R "$(id -un):" "$TMPDIR" + chmod -R go= "$TMPDIR" +fi + if [ $COMBINED_RUN_COMPARE -eq 0 ]; then # We first compare the output, so that even if the submission gets a # timelimit exceeded or runtime error verdict later, the jury can diff --git a/lib/.gitignore b/lib/.gitignore index 7963ca6c0d1..c585ac76095 100644 --- a/lib/.gitignore +++ b/lib/.gitignore @@ -1,4 +1,3 @@ /judge /submit -/vendor /dj_utils.py diff --git a/lib/Makefile b/lib/Makefile index 5f2e2dea763..238684c01f0 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -1,6 +1,7 @@ ifndef TOPDIR TOPDIR=.. endif + include $(TOPDIR)/Makefile.global OBJECTS = $(addsuffix $(OBJEXT),lib.error lib.misc) @@ -12,21 +13,9 @@ $(OBJECTS): %$(OBJEXT): %.c %.h clean-l: rm -f $(OBJECTS) -# Change baseDir in composer autogenerated files -define fix_composer_paths - for file in autoload_psr4.php autoload_classmap.php autoload_files.php autoload_namespaces.php ; do \ - sed -i "s#^\$$baseDir = .*#\$$baseDir = dirname('$(domserver_webappdir)');#" $(1)/composer/$$file ; \ - done - sed -i "s#__DIR__ \. '/\.\./\.\./\.\.' \. '/webapp#'$(domserver_webappdir)#" $(1)/composer/autoload_static.php -endef - install-domserver: $(INSTALL_DATA) -t $(DESTDIR)$(domserver_libdir) *.php $(INSTALL_PROG) -t $(DESTDIR)$(domserver_libdir) alert - for i in vendor/* ; do \ - $(call install_tree,$(DESTDIR)$(domserver_libvendordir),$$i) ; \ - done - $(call fix_composer_paths,$(DESTDIR)$(domserver_libvendordir)) install-judgehost: $(INSTALL_DATA) -t $(DESTDIR)$(judgehost_libdir) *.php *.sh diff --git a/lib/lib.misc.php b/lib/lib.misc.php index c9a68ee943c..33ffcb7cb59 100644 --- a/lib/lib.misc.php +++ b/lib/lib.misc.php @@ -75,7 +75,8 @@ function alert(string $msgtype, string $description = '') } /** - * Functions to support graceful shutdown of daemons upon receiving a signal + * Functions to support (graceful) shutdown of daemons upon receiving a + * signal. */ function sig_handler(int $signal, $siginfo = null) { @@ -85,10 +86,11 @@ function sig_handler(int $signal, $siginfo = null) switch ($signal) { case SIGHUP: - $gracefulexitsignalled = true; - // no break case SIGINT: # Ctrl+C case SIGTERM: + $gracefulexitsignalled = true; + // no break + case SIGQUIT: # Ctrl+/ $exitsignalled = true; } } @@ -106,12 +108,13 @@ function initsignals() logmsg(LOG_DEBUG, "Installing signal handlers"); - // Install signal handler for TERMINATE, HANGUP and INTERRUPT - // signals. The sleep() call will automatically return on - // receiving a signal. - pcntl_signal(SIGTERM, "sig_handler"); + // Install signal handler for HANGUP, INTERRUPT, QUIT and TERMINATE + // signals. All but the QUIT signal should trigger a graceful shutdown. + // The sleep() call will automatically return on receiving a signal. pcntl_signal(SIGHUP, "sig_handler"); pcntl_signal(SIGINT, "sig_handler"); + pcntl_signal(SIGQUIT, "sig_handler"); + pcntl_signal(SIGTERM, "sig_handler"); } /** diff --git a/misc-tools/configure-domjudge.in b/misc-tools/configure-domjudge.in index 9f649686e84..360bb8e163b 100755 --- a/misc-tools/configure-domjudge.in +++ b/misc-tools/configure-domjudge.in @@ -31,18 +31,22 @@ def usage(): exit(1) -def compare_configs(expected_config: Set, actual_config: Set, num_spaces=4) -> (List, Set, Set): +def compare_configs(expected_config: Set, actual_config: Set, num_spaces=4, key_mismatch_in_diff=False) -> (List, Set, Set): diffs = [] space_string = ' ' * num_spaces - for k in expected_config.keys(): - if k in actual_config and expected_config[k] != actual_config[k]: - if isinstance(expected_config[k], dict) and isinstance(actual_config[k], dict): - d, n, m = compare_configs(expected_config[k], actual_config[k], num_spaces=num_spaces+2) - if d: - diffs.append(f'{space_string}- {k}:') - diffs.extend(d) - else: - diffs.append(f'{space_string}- {k}:\n {space_string}is: {actual_config[k]}\n {space_string}new: {expected_config[k]}') + all_keys = set(expected_config.keys()) | set(actual_config.keys()) + for k in all_keys: + if k in expected_config and k in actual_config: + if expected_config[k] != actual_config[k]: + if isinstance(expected_config[k], dict) and isinstance(actual_config[k], dict): + d, _, _ = compare_configs(expected_config[k], actual_config[k], num_spaces=num_spaces+2, key_mismatch_in_diff=True) + if d: + diffs.append(f'{space_string}- {k}:') + diffs.extend(d) + else: + diffs.append(f'{space_string}- {k}:\n {space_string}is: {actual_config[k]}\n {space_string}new: {expected_config[k]}') + elif key_mismatch_in_diff: + diffs.append(f'{space_string}- {k}:\n {space_string}is: {actual_config.get(k, "")}\n {space_string}new: {expected_config.get(k, "")}') new_keys = set(expected_config.keys()).difference(set(actual_config.keys())) missing_keys = set(actual_config.keys()).difference(set(expected_config.keys())) @@ -130,7 +134,7 @@ if os.path.exists('languages.json'): dj_utils.upload_file(f'languages/{langid}/executable', 'executable', f'executables/{langid}.zip') for langid in executables: os.remove(f'executables/{langid}.zip') - if dj_utils.confirm(' - Upload configuration changes?', True): + if dj_utils.confirm(' - Upload language configuration changes?', True): actual_config = _keyify_list(dj_utils.upload_file(f'languages', 'json', f'languages.json')) diffs, new_keys, missing_keys = compare_configs( actual_config=actual_config, diff --git a/misc-tools/dj_utils.py b/misc-tools/dj_utils.py index 0d63cbd7142..c37a3508f45 100644 --- a/misc-tools/dj_utils.py +++ b/misc-tools/dj_utils.py @@ -11,6 +11,7 @@ import requests.utils import subprocess import sys +from urllib.parse import urlparse _myself = os.path.basename(sys.argv[0]) _default_user_agent = requests.utils.default_user_agent() @@ -76,12 +77,16 @@ def do_api_request(name: str, method: str = 'GET', jsonData: dict = {}): else: global ca_check url = f'{domjudge_webapp_folder_or_api_url}/{name}' + parsed = urlparse(domjudge_webapp_folder_or_api_url) + auth = None + if parsed.username or parsed.password: + auth = (parsed.username, parsed.password) try: if method == 'GET': - response = requests.get(url, headers=headers, verify=ca_check) + response = requests.get(url, headers=headers, verify=ca_check, auth=auth) elif method == 'PUT': - response = requests.put(url, headers=headers, verify=ca_check, json=jsonData) + response = requests.put(url, headers=headers, verify=ca_check, json=jsonData, auth=auth) except requests.exceptions.SSLError as e: ca_check = not confirm( "Can not verify certificate, ignore certificate check?", False) diff --git a/misc-tools/import-contest.in b/misc-tools/import-contest.in index 60b55d066da..fbf080b5f5d 100755 --- a/misc-tools/import-contest.in +++ b/misc-tools/import-contest.in @@ -108,12 +108,30 @@ def import_contest_banner(cid: str): break if banner_file: - if dj_utils.confirm(f'Import {banner_file} for contest?', False): + if dj_utils.confirm(f'Import {banner_file} for contest?', True): dj_utils.upload_file(f'contests/{cid}/banner', 'banner', banner_file) print('Contest banner imported.') else: print('Skipping contest banner import.') +def import_contest_problemset_document(cid: str): + """Import the contest problemset document""" + + files = ['problemset.pdf', 'contest.pdf', 'contest-web.pdf', 'contest.html', 'contest.txt'] + + text_file = None + for file in files: + if os.path.isfile(file): + text_file = file + break + + if text_file: + if dj_utils.confirm(f'Import {text_file} for contest?', False): + dj_utils.upload_file(f'contests/{cid}/problemset', 'problemset', text_file) + print('Contest problemset imported.') + else: + print('Skipping contest problemset import.') + if len(sys.argv) == 1: dj_utils.domjudge_webapp_folder_or_api_url = webappdir elif len(sys.argv) == 2: @@ -131,10 +149,10 @@ if import_file('organizations', ['organizations.json']): # Also import logos if we have any # We prefer the 64x64 logo. If it doesn't exist, accept a generic logo (which might be a SVG) # We also prefer PNG/SVG before JPG - import_images('organizations', 'logo', ['^logo\.64x\d+\.png$', '^logo\.(png|svg)$', '^logo\.64x\d+\.jpg$', '^logo\.jpg$']) + import_images('organizations', 'logo', ['^logo\\.64x\\d+\\.png$', '^logo\\.(png|svg)$', '^logo\\.64x\\d+\\.jpg$', '^logo\\.jpg$']) if import_file('teams', ['teams.json', 'teams2.tsv']): # Also import photos if we have any, but prefer JPG over SVG and PNG - import_images('teams', 'photo', ['^photo\.jpg$', '^photo\.(png|svg)$']) + import_images('teams', 'photo', ['^photo\\.jpg$', '^photo\\.(png|svg)$']) import_file('accounts', ['accounts.json', 'accounts.yaml', 'accounts.tsv']) problems_imported = False @@ -154,6 +172,7 @@ else: if cid is not None: print(f' -> cid={cid}') import_contest_banner(cid) + import_contest_problemset_document(cid) # Problem import is also special: we need to upload each individual problem and detect what they are if os.path.exists('problems.yaml') or os.path.exists('problems.json') or os.path.exists('problemset.yaml'): @@ -178,11 +197,6 @@ if os.path.exists('problems.yaml') or os.path.exists('problems.json') or os.path problems_file = 'problemset.yaml' dj_utils.upload_file(f'contests/{cid}/problems/add-data', 'data', problems_file) - # We might need to translate the problem external ID's into an internal ID (when we are in data source = local mode) - # For this, we get the problems from the API and create a dict with the mapping - problem_mapping = {problem['externalid']: problem['id'] - for problem in dj_utils.do_api_request(f'contests/{cid}/problems')} - if os.path.exists('problems.yaml'): with open('problems.yaml') as problemFile: problemData = yaml.safe_load(problemFile) @@ -207,11 +221,10 @@ if os.path.exists('problems.yaml') or os.path.exists('problems.json') or os.path exit(3) os.system(f'cd {problem} && zip -r \'../{problem}\' -- .timelimit *') - problem_id = problem_mapping[problem] - if ((not confirmIndividually) or dj_utils.confirm(f'Ready to import problem \'{problem}\' to probid={problem_id}. Continue?', True)): + if ((not confirmIndividually) or dj_utils.confirm(f'Ready to import problem \'{problem}\' to problem={problem}. Continue?', True)): print(f'Uploading problem \'{problem}\', please be patient, this may take a while.') response = dj_utils.upload_file( - f'contests/{cid}/problems', 'zip', f'{problem}.zip', {'problem': problem_id}) + f'contests/{cid}/problems', 'zip', f'{problem}.zip', {'problem': problem}) print(json.dumps(response, indent=4)) else: print('Skipping contest import.') diff --git a/paths.mk.in b/paths.mk.in index c0e9d4feadd..be51100deb9 100644 --- a/paths.mk.in +++ b/paths.mk.in @@ -86,7 +86,6 @@ domserver_etcdir = @domserver_etcdir@ domserver_webappdir = @domserver_webappdir@ domserver_sqldir = @domserver_sqldir@ domserver_libdir = @domserver_libdir@ -domserver_libvendordir = @domserver_libvendordir@ domserver_logdir = @domserver_logdir@ domserver_rundir = @domserver_rundir@ domserver_tmpdir = @domserver_tmpdir@ @@ -103,7 +102,6 @@ judgehost_rundir = @judgehost_rundir@ judgehost_tmpdir = @judgehost_tmpdir@ judgehost_judgedir = @judgehost_judgedir@ judgehost_chrootdir = @judgehost_chrootdir@ -judgehost_cgroupdir = @judgehost_cgroupdir@ domjudge_docdir = @domjudge_docdir@ @@ -112,8 +110,7 @@ systemd_unitdir = @systemd_unitdir@ # The tmpdir's are not in these lists, since they would otherwise get # their permissions overwritten in FHS install mode. domserver_dirs = $(domserver_bindir) $(domserver_etcdir) \ - $(domserver_libdir) $(domserver_libvendordir) \ - $(domserver_logdir) $(domserver_rundir) \ + $(domserver_libdir) $(domserver_logdir) $(domserver_rundir) \ $(addprefix $(domserver_webappdir)/public/images/,affiliations countries teams) \ $(domserver_exampleprobdir) $(domserver_databasedumpdir) @@ -138,7 +135,6 @@ define substconfigvars -e 's,@domserver_webappdir[@],@domserver_webappdir@,g' \ -e 's,@domserver_sqldir[@],@domserver_sqldir@,g' \ -e 's,@domserver_libdir[@],@domserver_libdir@,g' \ - -e 's,@domserver_libvendordir[@],@domserver_libvendordir@,g' \ -e 's,@domserver_logdir[@],@domserver_logdir@,g' \ -e 's,@domserver_rundir[@],@domserver_rundir@,g' \ -e 's,@domserver_tmpdir[@],@domserver_tmpdir@,g' \ @@ -153,7 +149,6 @@ define substconfigvars -e 's,@judgehost_tmpdir[@],@judgehost_tmpdir@,g' \ -e 's,@judgehost_judgedir[@],@judgehost_judgedir@,g' \ -e 's,@judgehost_chrootdir[@],@judgehost_chrootdir@,g' \ - -e 's,@judgehost_cgroupdir[@],@judgehost_cgroupdir@,g' \ -e 's,@domjudge_docdir[@],@domjudge_docdir@,g' \ -e 's,@systemd_unitdir[@],@systemd_unitdir@,g' \ -e 's,@DOMJUDGE_USER[@],@DOMJUDGE_USER@,g' \ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 475ff17ecc0..00000000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,70 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Method App\\\\Controller\\\\API\\\\AbstractRestController\\:\\:listActionHelper\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/AbstractRestController.php - - - - message: "#^PHPDoc tag @var for variable \\$objects has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/AbstractRestController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:addProblemAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/GeneralInfoController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:getDatabaseConfigurationAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/GeneralInfoController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:updateConfigurationAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/GeneralInfoController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:checkVersions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:createJudgehostAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getVersionCommands\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:updateJudgeHostAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\ProblemController\\:\\:addProblemAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/ProblemController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\ProblemController\\:\\:addProblemsAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/ProblemController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\ProblemController\\:\\:transformObject\\(\\) has parameter \\$object with no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/ProblemController.php - - - message: "#^Method App\\\\FosRestBundle\\\\FlattenExceptionHandler\\:\\:serializeToJson\\(\\) has parameter \\$type with no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/FosRestBundle/FlattenExceptionHandler.php - - - - message: "#^Method App\\\\FosRestBundle\\\\FlattenExceptionHandler\\:\\:serializeToJson\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/FosRestBundle/FlattenExceptionHandler.php diff --git a/sql/files/defaultdata/awk/run b/sql/files/defaultdata/awk/run index 7cdb47508a3..1786816dc7a 100755 --- a/sql/files/defaultdata/awk/run +++ b/sql/files/defaultdata/awk/run @@ -13,7 +13,12 @@ DEST="$1" ; shift MEMLIMIT="$1" ; shift MAINSOURCE="$1" -# There is no portable way to test the syntax of an awk script. +# Syntax check based on: https://stackoverflow.com/a/7212314 +for j in "$@" ; do + awk -f $j -- 'BEGIN { exit(0) } END { exit(0) }' + EXITCODE=$? + [ "$EXITCODE" -ne 0 ] && exit $EXITCODE +done # We construct here the list of source files to be passed to awk: FILEARGS='' diff --git a/sql/files/defaultdata/hs/run b/sql/files/defaultdata/hs/run index d48e2930eb3..736de457a54 100755 --- a/sql/files/defaultdata/hs/run +++ b/sql/files/defaultdata/hs/run @@ -10,6 +10,9 @@ MAINSOURCE="$1" # Set non-existing HOME variable to make GHC program happy, see: # https://ghc.haskell.org/trac/ghc/ticket/11678 export HOME=/does/not/exist +# Allow temporary files during compilation, this directory is not +# available during the submission run. +export TMPDIR="$PWD" # Add -DONLINE_JUDGE or -DDOMJUDGE below if you want it make easier for teams # to do local debugging. diff --git a/sql/files/defaultdata/js/run b/sql/files/defaultdata/js/run index 9ee00657034..aceb0980d79 100755 --- a/sql/files/defaultdata/js/run +++ b/sql/files/defaultdata/js/run @@ -20,6 +20,15 @@ if [ -z "$ENTRY_POINT" ]; then echo "Info: detected entry_point: $MAINSOURCE" fi +# Run syntax check +for file in "$@"; do + case $file in *.js) + nodejs --check "$file" + EXITCODE="$?" + [ "$EXITCODE" -ne 0 ] && exit $EXITCODE ;; + esac +done + # Write executing script: cat > "$DEST" <&2 exit 1 fi +# Check syntax +# +# Store intermediate files in the current dir (/compile) as its only +# available during compilation step. +export TMPDIR=`pwd` +Rscript -e "parse('"$@"')" +EXITCODE=$? +[ "$EXITCODE" -ne 0 ] && exit $EXITCODE # Write executing script: cat > "$DEST" < phpunit/phpunit ### /phpunit.xml diff --git a/webapp/Makefile b/webapp/Makefile index 621c63f2417..cda15366782 100644 --- a/webapp/Makefile +++ b/webapp/Makefile @@ -7,13 +7,26 @@ REC_TARGETS = domserver include $(TOPDIR)/Makefile.global # Subdirectories to recurse into for REC_TARGETS -SUBDIRS = config +SUBDIRS = config vendor + +maintainer-conf: .env.local + +# Run Symfony in dev mode (for maintainer-mode): +.env.local: + @echo "Creating file '$@'..." + @echo "# This file was automatically created by 'make maintainer-conf' to run" > $@ + @echo "# the DOMjudge Symfony application in developer mode. Adjust as needed." >> $@ + @echo "APP_ENV=dev" >> $@ + +composer-dump-autoload-dev: + composer $(subst 1,-q,$(QUIET)) dump-autoload public/bundles/nelmioapidoc: # We can not use bin/console here, as when using a fakeroot, # the include paths are broken. We just copy in the data we need - mkdir -p $@ - cp -a ../lib/vendor/nelmio/api-doc-bundle/Resources/public/* $@ + -rm -rf public/bundles/nelmioapidoc + mkdir -p public/bundles/nelmioapidoc + cp -R vendor/nelmio/api-doc-bundle/public/* public/bundles/nelmioapidoc/ clean-l: -rm -rf public/bundles/nelmioapidoc @@ -22,7 +35,7 @@ domserver-l: public/bundles/nelmioapidoc install-domserver: $(INSTALL_DIR) $(DESTDIR)$(domserver_webappdir); - for d in bin config migrations public resources src templates tests ; do \ + for d in bin config migrations public resources src templates tests vendor; do \ $(call install_tree,$(DESTDIR)$(domserver_webappdir),$$d) ; \ done # Change webapp/public/doc symlink @@ -31,14 +44,33 @@ install-domserver: # Now change all relative symlinks in webapp/public to their correct paths for link in $$(find $(DESTDIR)$(domserver_webappdir)/public/$$dir -maxdepth 2 -type l); do \ target=$$(readlink $$link) ; \ - if echo $${target} | grep -q '\.\./\.\./lib/vendor' ; then \ + case $${target} in *../vendor*) \ rm $$link ; \ - realtarget=$(domserver_libvendordir)$$(echo $${target} | sed 's!^.*\.\./\.\./lib/vendor!!') ; \ + realtarget=$(domserver_webappdir)/vendor$${target#*../vendor} ; \ ln -s $$realtarget $$link ; \ - fi \ + esac \ done $(INSTALL_DATA) -t $(DESTDIR)$(domserver_webappdir) phpunit.xml.dist .env +inplace-install: composer-autoclean +maintainer-install: composer-dump-autoload-dev + +# Install PHP dependencies +composer-dependencies: +ifeq (, $(shell command -v composer 2> /dev/null)) + $(error "'composer' command not found in $(PATH), install it via your package manager or https://getcomposer.org/download/") +endif +# We use --no-scripts here because at this point the autoload.php file is +# not generated yet, which is needed to run the post-install scripts. + composer $(subst 1,-q,$(QUIET)) install --prefer-dist -o -a --no-scripts --no-plugins + +composer-dependencies-dev: + composer $(subst 1,-q,$(QUIET)) install --prefer-dist --no-scripts --no-plugins + +composer-autoclean: +# Make sure we're running from a clean state: + composer auto-scripts + maintainer-clean-l: -for d in cache log ; do \ for t in dev prod ; do \ diff --git a/webapp/bin/console b/webapp/bin/console index 87a4a429ed3..ec90dd3d58a 100755 --- a/webapp/bin/console +++ b/webapp/bin/console @@ -4,11 +4,11 @@ use App\Kernel; use Symfony\Bundle\FrameworkBundle\Console\Application; -if (!is_file(dirname(__DIR__, 2) . '/lib/vendor/autoload_runtime.php')) { +if (!is_file(dirname(__DIR__) . '/vendor/autoload_runtime.php')) { throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".'); } -require_once dirname(__DIR__, 2) . '/lib/vendor/autoload_runtime.php'; +require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; require_once dirname(__DIR__) . '/config/load_db_secrets.php'; set_time_limit(0); diff --git a/webapp/bin/phpunit b/webapp/bin/phpunit index ebac76f8572..cefea84bfa2 100755 --- a/webapp/bin/phpunit +++ b/webapp/bin/phpunit @@ -5,20 +5,19 @@ if (!ini_get('date.timezone')) { ini_set('date.timezone', 'UTC'); } -if (is_file(dirname(__DIR__, 2) . '/lib/vendor/phpunit/phpunit/phpunit')) { +if (is_file(dirname(__DIR__) . '/vendor/phpunit/phpunit/phpunit')) { if (PHP_VERSION_ID >= 80000) { - require dirname(__DIR__, 2) . '/lib/vendor/phpunit/phpunit/phpunit'; + require dirname(__DIR__) . '/vendor/phpunit/phpunit/phpunit'; } else { - define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__, 2) . '/lib/vendor/autoload.php'); + define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__) . '/vendor/autoload.php'); require PHPUNIT_COMPOSER_INSTALL; PHPUnit\TextUI\Command::main(); } } else { - if (!is_file(dirname(__DIR__, - 2) . '/lib/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { + if (!is_file(dirname(__DIR__) . '/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; exit(1); } - require dirname(__DIR__, 2) . '/lib/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; + require dirname(__DIR__) . '/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; } diff --git a/composer.json b/webapp/composer.json similarity index 93% rename from composer.json rename to webapp/composer.json index 05495bef825..124580fc0a3 100644 --- a/composer.json +++ b/webapp/composer.json @@ -8,9 +8,9 @@ "type": "package", "package": { "name": "fortawesome/font-awesome", - "version": "6.4.2", + "version": "6.5.2", "dist": { - "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.4.2/fontawesome-free-6.4.2-web.zip", + "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.2/fontawesome-free-6.5.2-web.zip", "type": "zip" } } @@ -70,11 +70,13 @@ "league/commonmark": "^2.3", "mbostock/d3": "^3.5", "nelmio/api-doc-bundle": "^4.11", + "nelmio/cors-bundle": "^2.4", "novus/nvd3": "^1.8", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.25", "promphp/prometheus_client_php": "^2.6", "ramsey/uuid": "^4.2", + "riverline/multipart-parser": "^2.1", "select2/select2": "4.*", "sentry/sentry-symfony": "^4.5", "symfony/asset": "6.4.*", @@ -123,15 +125,15 @@ }, "autoload": { "psr-4": { - "App\\": "webapp/src/" + "App\\": "src/" }, "files": [ - "webapp/resources/functions.php" + "resources/functions.php" ] }, "autoload-dev": { "psr-4": { - "App\\Tests\\": "webapp/tests/" + "App\\Tests\\": "tests/" } }, "config": { @@ -142,8 +144,6 @@ "platform": { "php": "8.1.0" }, - "vendor-dir": "lib/vendor", - "component-dir": "lib/vendor/components", "allow-plugins": { "composer/package-versions-deprecated": true, "symfony/flex": true, @@ -176,12 +176,8 @@ }, "extra": { "symfony": { - "root-dir": "webapp/", "allow-contrib": true, "require": "6.4.*" - }, - "runtime": { - "dotenv_path": "webapp/.env" } } } diff --git a/composer.lock b/webapp/composer.lock similarity index 89% rename from composer.lock rename to webapp/composer.lock index b4654dcf7ad..4e8d6c0e92e 100644 --- a/composer.lock +++ b/webapp/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "46b6797a184b1ae6a2c1bf8f577fb445", + "content-hash": "3ffb9ae6009d6e668f77b90a4c6d5cf5", "packages": [ { "name": "apalfrey/select2-bootstrap-5-theme", @@ -50,25 +50,25 @@ }, { "name": "brick/math", - "version": "0.11.0", + "version": "0.12.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478" + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478", - "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", "shasum": "" }, "require": { - "php": "^8.0" + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^9.0", - "vimeo/psalm": "5.0.0" + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" }, "type": "library", "autoload": { @@ -88,12 +88,17 @@ "arithmetic", "bigdecimal", "bignum", + "bignumber", "brick", - "math" + "decimal", + "integer", + "math", + "mathematics", + "rational" ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.11.0" + "source": "https://github.com/brick/math/tree/0.12.1" }, "funding": [ { @@ -101,7 +106,7 @@ "type": "github" } ], - "time": "2023-01-15T23:15:59+00:00" + "time": "2023-11-29T23:19:16+00:00" }, { "name": "clue/stream-filter", @@ -265,12 +270,12 @@ "source": { "type": "git", "url": "https://github.com/DataTables/Dist-DataTables.git", - "reference": "8da2d44fb0515635f03a7e4cb8b6441a790d8ef2" + "reference": "5a1078977242eef31741a82bacd516cde9568275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DataTables/Dist-DataTables/zipball/8da2d44fb0515635f03a7e4cb8b6441a790d8ef2", - "reference": "8da2d44fb0515635f03a7e4cb8b6441a790d8ef2", + "url": "https://api.github.com/repos/DataTables/Dist-DataTables/zipball/5a1078977242eef31741a82bacd516cde9568275", + "reference": "5a1078977242eef31741a82bacd516cde9568275", "shasum": "" }, "require": { @@ -299,7 +304,7 @@ "forum": "https://datatables.net/forums", "source": "https://github.com/DataTables/Dist-DataTables" }, - "time": "2024-01-25T11:59:45+00:00" + "time": "2024-06-17T10:20:23+00:00" }, { "name": "datatables.net/datatables.net-bs5", @@ -307,12 +312,12 @@ "source": { "type": "git", "url": "https://github.com/DataTables/Dist-DataTables-Bootstrap5.git", - "reference": "5a819ad3495508797a12d4bdcaa729b7114c65e9" + "reference": "598c1fcced6a8dd64661109f7398975fc23bfb97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DataTables/Dist-DataTables-Bootstrap5/zipball/5a819ad3495508797a12d4bdcaa729b7114c65e9", - "reference": "5a819ad3495508797a12d4bdcaa729b7114c65e9", + "url": "https://api.github.com/repos/DataTables/Dist-DataTables-Bootstrap5/zipball/598c1fcced6a8dd64661109f7398975fc23bfb97", + "reference": "598c1fcced6a8dd64661109f7398975fc23bfb97", "shasum": "" }, "require": { @@ -345,7 +350,7 @@ "issues": "https://github.com/DataTables/Dist-DataTables-Bootstrap5/issues", "source": "https://github.com/DataTables/Dist-DataTables-Bootstrap5" }, - "time": "2024-01-25T11:58:19+00:00" + "time": "2024-06-17T10:20:47+00:00" }, { "name": "dflydev/dot-access-data", @@ -422,82 +427,6 @@ }, "time": "2022-10-27T11:44:00+00:00" }, - { - "name": "doctrine/annotations", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", - "shasum": "" - }, - "require": { - "doctrine/lexer": "^2 || ^3", - "ext-tokenizer": "*", - "php": "^7.2 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" - }, - "require-dev": { - "doctrine/cache": "^2.0", - "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.8.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^5.4 || ^6", - "vimeo/psalm": "^4.10" - }, - "suggest": { - "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/2.0.1" - }, - "time": "2023-02-02T22:02:53+00:00" - }, { "name": "doctrine/cache", "version": "2.2.0", @@ -593,16 +522,16 @@ }, { "name": "doctrine/collections", - "version": "2.1.4", + "version": "2.2.2", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "72328a11443a0de79967104ad36ba7b30bded134" + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/72328a11443a0de79967104ad36ba7b30bded134", - "reference": "72328a11443a0de79967104ad36ba7b30bded134", + "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", "shasum": "" }, "require": { @@ -614,7 +543,7 @@ "ext-json": "*", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^10.5", "vimeo/psalm": "^5.11" }, "type": "library", @@ -659,7 +588,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.1.4" + "source": "https://github.com/doctrine/collections/tree/2.2.2" }, "funding": [ { @@ -675,20 +604,20 @@ "type": "tidelift" } ], - "time": "2023-10-03T09:22:33+00:00" + "time": "2024-04-18T06:56:21+00:00" }, { "name": "doctrine/common", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" + "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", + "url": "https://api.github.com/repos/doctrine/common/zipball/0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", + "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", "shasum": "" }, "require": { @@ -750,7 +679,7 @@ ], "support": { "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.3" + "source": "https://github.com/doctrine/common/tree/3.4.4" }, "funding": [ { @@ -766,7 +695,7 @@ "type": "tidelift" } ], - "time": "2022-10-09T11:47:59+00:00" + "time": "2024-04-16T13:35:33+00:00" }, { "name": "doctrine/data-fixtures", @@ -854,16 +783,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.0", + "version": "3.8.6", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9" + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d244f2e6e6bf32bff5174e6729b57214923ecec9", - "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1", + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1", "shasum": "" }, "require": { @@ -879,12 +808,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.56", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.15", + "phpstan/phpstan": "1.11.5", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.19", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.8.1", + "squizlabs/php_codesniffer": "3.10.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -947,7 +876,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.0" + "source": "https://github.com/doctrine/dbal/tree/3.8.6" }, "funding": [ { @@ -963,7 +892,7 @@ "type": "tidelift" } ], - "time": "2024-01-25T21:44:02+00:00" + "time": "2024-06-19T10:38:17+00:00" }, { "name": "doctrine/deprecations", @@ -1014,16 +943,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.11.1", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "4089f1424b724786c062aea50aae5f773449b94b" + "reference": "5418e811a14724068e95e0ba43353b903ada530f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/4089f1424b724786c062aea50aae5f773449b94b", - "reference": "4089f1424b724786c062aea50aae5f773449b94b", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5418e811a14724068e95e0ba43353b903ada530f", + "reference": "5418e811a14724068e95e0ba43353b903ada530f", "shasum": "" }, "require": { @@ -1044,23 +973,24 @@ }, "conflict": { "doctrine/annotations": ">=3.0", - "doctrine/orm": "<2.14 || >=4.0", + "doctrine/orm": "<2.17 || >=4.0", "twig/twig": "<1.34 || >=2.0 <2.4" }, "require-dev": { "doctrine/annotations": "^1 || ^2", "doctrine/coding-standard": "^12", "doctrine/deprecations": "^1.0", - "doctrine/orm": "^2.14 || ^3.0", + "doctrine/orm": "^2.17 || ^3.0", "friendsofphp/proxy-manager-lts": "^1.0", - "phpunit/phpunit": "^9.5.26 || ^10.0", + "phpunit/phpunit": "^9.5.26", "psalm/plugin-phpunit": "^0.18.4", - "psalm/plugin-symfony": "^4", + "psalm/plugin-symfony": "^5", "psr/log": "^1.1.4 || ^2.0 || ^3.0", "symfony/phpunit-bridge": "^6.1 || ^7.0", "symfony/property-info": "^5.4 || ^6.0 || ^7.0", "symfony/proxy-manager-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", "symfony/string": "^5.4 || ^6.0 || ^7.0", "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/validator": "^5.4 || ^6.0 || ^7.0", @@ -1068,7 +998,7 @@ "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0", "twig/twig": "^1.34 || ^2.12 || ^3.0", - "vimeo/psalm": "^4.30" + "vimeo/psalm": "^5.15" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -1078,7 +1008,7 @@ "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\DoctrineBundle\\": "" + "Doctrine\\Bundle\\DoctrineBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1113,7 +1043,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.11.1" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.12.0" }, "funding": [ { @@ -1129,20 +1059,20 @@ "type": "tidelift" } ], - "time": "2023-11-15T20:01:50+00:00" + "time": "2024-03-19T07:20:37+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.5.1", + "version": "3.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d" + "reference": "d13a08ebf244f74c8adb8ff15aa55d01c404e534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/c808a0c85c38c8ee265cc8405b456c1d2b38567d", - "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/d13a08ebf244f74c8adb8ff15aa55d01c404e534", + "reference": "d13a08ebf244f74c8adb8ff15aa55d01c404e534", "shasum": "" }, "require": { @@ -1171,7 +1101,7 @@ "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\FixturesBundle\\": "" + "Doctrine\\Bundle\\FixturesBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1200,7 +1130,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.5.1" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.6.1" }, "funding": [ { @@ -1216,20 +1146,20 @@ "type": "tidelift" } ], - "time": "2023-11-19T12:48:54+00:00" + "time": "2024-05-07T07:16:35+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "1dd42906a5fb9c5960723e2ebb45c68006493835" + "reference": "715b62c31a5894afcb2b2cdbbc6607d7dd0580c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/1dd42906a5fb9c5960723e2ebb45c68006493835", - "reference": "1dd42906a5fb9c5960723e2ebb45c68006493835", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/715b62c31a5894afcb2b2cdbbc6607d7dd0580c0", + "reference": "715b62c31a5894afcb2b2cdbbc6607d7dd0580c0", "shasum": "" }, "require": { @@ -1240,6 +1170,7 @@ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { + "composer/semver": "^3.0", "doctrine/coding-standard": "^12", "doctrine/orm": "^2.6 || ^3", "doctrine/persistence": "^2.0 || ^3 ", @@ -1291,7 +1222,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.3.0" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.3.1" }, "funding": [ { @@ -1307,20 +1238,20 @@ "type": "tidelift" } ], - "time": "2023-11-13T19:44:41+00:00" + "time": "2024-05-14T20:32:18+00:00" }, { "name": "doctrine/event-manager", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32" + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/750671534e0241a7c50ea5b43f67e23eb5c96f32", - "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", "shasum": "" }, "require": { @@ -1330,10 +1261,10 @@ "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^10", + "doctrine/coding-standard": "^12", "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^4.28" + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "type": "library", "autoload": { @@ -1382,7 +1313,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.0" + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" }, "funding": [ { @@ -1398,20 +1329,20 @@ "type": "tidelift" } ], - "time": "2022-10-12T20:59:15+00:00" + "time": "2024-05-22T20:47:39+00:00" }, { "name": "doctrine/inflector", - "version": "2.0.9", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65" + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/2930cd5ef353871c821d5c43ed030d39ac8cfe65", - "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", "shasum": "" }, "require": { @@ -1473,7 +1404,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.9" + "source": "https://github.com/doctrine/inflector/tree/2.0.10" }, "funding": [ { @@ -1489,7 +1420,7 @@ "type": "tidelift" } ], - "time": "2024-01-15T18:05:13+00:00" + "time": "2024-02-18T20:23:39+00:00" }, { "name": "doctrine/instantiator", @@ -1563,28 +1494,27 @@ }, { "name": "doctrine/lexer", - "version": "2.1.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.0" + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { @@ -1621,7 +1551,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.0" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -1637,20 +1567,20 @@ "type": "tidelift" } ], - "time": "2022-12-14T08:49:07+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "doctrine/migrations", - "version": "3.7.2", + "version": "3.7.4", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "47af29eef49f29ebee545947e8b2a4b3be318c8a" + "reference": "954e0a314c2f0eb9fb418210445111747de254a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/47af29eef49f29ebee545947e8b2a4b3be318c8a", - "reference": "47af29eef49f29ebee545947e8b2a4b3be318c8a", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/954e0a314c2f0eb9fb418210445111747de254a6", + "reference": "954e0a314c2f0eb9fb418210445111747de254a6", "shasum": "" }, "require": { @@ -1723,7 +1653,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.7.2" + "source": "https://github.com/doctrine/migrations/tree/3.7.4" }, "funding": [ { @@ -1739,20 +1669,20 @@ "type": "tidelift" } ], - "time": "2023-12-05T11:35:05+00:00" + "time": "2024-03-06T13:41:11+00:00" }, { "name": "doctrine/orm", - "version": "2.17.4", + "version": "2.19.5", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d" + "reference": "94986af28452da42a46a4489d1c958a2e5d710e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/ccfc97c32f63aaa0988ac6aa42e71c5590bb794d", - "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d", + "url": "https://api.github.com/repos/doctrine/orm/zipball/94986af28452da42a46a4489d1c958a2e5d710e5", + "reference": "94986af28452da42a46a4489d1c958a2e5d710e5", "shasum": "" }, "require": { @@ -1765,7 +1695,7 @@ "doctrine/event-manager": "^1.2 || ^2", "doctrine/inflector": "^1.4 || ^2.0", "doctrine/instantiator": "^1.3 || ^2", - "doctrine/lexer": "^2", + "doctrine/lexer": "^2 || ^3", "doctrine/persistence": "^2.4 || ^3", "ext-ctype": "*", "php": "^7.1 || ^8.0", @@ -1781,14 +1711,14 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.10.35", + "phpstan/phpstan": "~1.4.10 || 1.10.59", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "vimeo/psalm": "4.30.0 || 5.16.0" + "vimeo/psalm": "4.30.0 || 5.22.2" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1801,7 +1731,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\ORM\\": "lib/Doctrine/ORM" + "Doctrine\\ORM\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1838,22 +1768,22 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.17.4" + "source": "https://github.com/doctrine/orm/tree/2.19.5" }, - "time": "2024-01-26T19:41:16+00:00" + "time": "2024-04-30T06:49:54+00:00" }, { "name": "doctrine/persistence", - "version": "3.2.0", + "version": "3.3.3", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "63fee8c33bef740db6730eb2a750cd3da6495603" + "reference": "b337726451f5d530df338fc7f68dee8781b49779" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/63fee8c33bef740db6730eb2a750cd3da6495603", - "reference": "63fee8c33bef740db6730eb2a750cd3da6495603", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/b337726451f5d530df338fc7f68dee8781b49779", + "reference": "b337726451f5d530df338fc7f68dee8781b49779", "shasum": "" }, "require": { @@ -1865,15 +1795,14 @@ "doctrine/common": "<2.10" }, "require-dev": { - "composer/package-versions-deprecated": "^1.11", - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^12", "doctrine/common": "^3.0", - "phpstan/phpstan": "1.9.4", + "phpstan/phpstan": "1.11.1", "phpstan/phpstan-phpunit": "^1", "phpstan/phpstan-strict-rules": "^1.1", "phpunit/phpunit": "^8.5 || ^9.5", "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.3.0" + "vimeo/psalm": "4.30.0 || 5.24.0" }, "type": "library", "autoload": { @@ -1922,7 +1851,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.2.0" + "source": "https://github.com/doctrine/persistence/tree/3.3.3" }, "funding": [ { @@ -1938,27 +1867,30 @@ "type": "tidelift" } ], - "time": "2023-05-17T18:32:04+00:00" + "time": "2024-06-20T10:14:30+00:00" }, { "name": "doctrine/sql-formatter", - "version": "1.1.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5" + "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/25a06c7bf4c6b8218f47928654252863ffc890a5", - "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d1ac84aef745c69ea034929eb6d65a6908b675cc", + "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "bin": [ "bin/sql-formatter" @@ -1988,9 +1920,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.1.3" + "source": "https://github.com/doctrine/sql-formatter/tree/1.4.0" }, - "time": "2022-05-23T21:33:49+00:00" + "time": "2024-05-08T08:12:09+00:00" }, { "name": "eligrey/filesaver", @@ -2003,37 +1935,38 @@ }, { "name": "fortawesome/font-awesome", - "version": "6.4.2", + "version": "6.5.2", "dist": { "type": "zip", - "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.4.2/fontawesome-free-6.4.2-web.zip" + "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.2/fontawesome-free-6.5.2-web.zip" }, "type": "library" }, { "name": "friendsofsymfony/rest-bundle", - "version": "3.6.0", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git", - "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179" + "reference": "db7d9a17da2bcae1bb8e2d7ff320ef3915903373" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/e01be8113d4451adb3cbb29d7d2cc96bbc698179", - "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/db7d9a17da2bcae1bb8e2d7ff320ef3915903373", + "reference": "db7d9a17da2bcae1bb8e2d7ff320ef3915903373", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/framework-bundle": "^4.4.1|^5.0|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/routing": "^5.4|^6.0", - "symfony/security-core": "^5.4|^6.0", + "php": "^7.4|^8.0", + "symfony/config": "^5.4|^6.4|^7.0", + "symfony/dependency-injection": "^5.4|^6.4|^7.0", + "symfony/deprecation-contracts": "^2.1|^3.0", + "symfony/event-dispatcher": "^5.4|^6.4|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/http-foundation": "^5.4|^6.4|^7.0", + "symfony/http-kernel": "^5.4|^6.4|^7.0", + "symfony/routing": "^5.4|^6.4|^7.0", + "symfony/security-core": "^5.4|^6.4|^7.0", "willdurand/jsonp-callback-validator": "^1.0|^2.0", "willdurand/negotiation": "^2.0|^3.0" }, @@ -2044,32 +1977,32 @@ "sensio/framework-extra-bundle": "<6.1" }, "require-dev": { - "doctrine/annotations": "^1.13.2|^2.0 ", - "friendsofphp/php-cs-fixer": "^3.0", + "doctrine/annotations": "^1.13.2|^2.0", + "friendsofphp/php-cs-fixer": "^3.43", "jms/serializer": "^1.13|^2.0|^3.0", "jms/serializer-bundle": "^2.4.3|^3.0.1|^4.0|^5.0", "psr/http-message": "^1.0", "psr/log": "^1.0|^2.0|^3.0", "sensio/framework-extra-bundle": "^6.1", - "symfony/asset": "^5.4|^6.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/css-selector": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/form": "^5.4|^6.0", - "symfony/mime": "^5.4|^6.0", - "symfony/phpunit-bridge": "^5.4|^6.0", - "symfony/security-bundle": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0", - "symfony/twig-bundle": "^5.4|^6.0", - "symfony/validator": "^5.4|^6.0", - "symfony/web-profiler-bundle": "^5.4|^6.0", - "symfony/yaml": "^5.4|^6.0" + "symfony/asset": "^5.4|^6.4|^7.0", + "symfony/browser-kit": "^5.4|^6.4|^7.0", + "symfony/css-selector": "^5.4|^6.4|^7.0", + "symfony/expression-language": "^5.4|^6.4|^7.0", + "symfony/form": "^5.4|^6.4|^7.0", + "symfony/mime": "^5.4|^6.4|^7.0", + "symfony/phpunit-bridge": "^7.0.1", + "symfony/security-bundle": "^5.4|^6.4|^7.0", + "symfony/serializer": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "symfony/validator": "^5.4|^6.4|^7.0", + "symfony/web-profiler-bundle": "^5.4|^6.4|^7.0", + "symfony/yaml": "^5.4|^6.4|^7.0" }, "suggest": { - "jms/serializer-bundle": "Add support for advanced serialization capabilities, recommended, requires ^2.0|^3.0", - "sensio/framework-extra-bundle": "Add support for the request body converter and the view response listener, requires ^3.0", - "symfony/serializer": "Add support for basic serialization capabilities and xml decoding, requires ^2.7|^3.0", - "symfony/validator": "Add support for validation capabilities in the ParamFetcher, requires ^2.7|^3.0" + "jms/serializer-bundle": "Add support for advanced serialization capabilities, recommended", + "sensio/framework-extra-bundle": "Add support for the request body converter and the view response listener, not supported with Symfony >=7.0", + "symfony/serializer": "Add support for basic serialization capabilities and xml decoding", + "symfony/validator": "Add support for validation capabilities in the ParamFetcher" }, "type": "symfony-bundle", "extra": { @@ -2111,9 +2044,9 @@ ], "support": { "issues": "https://github.com/FriendsOfSymfony/FOSRestBundle/issues", - "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.6.0" + "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.7.1" }, - "time": "2023-09-27T11:41:02+00:00" + "time": "2024-04-12T22:57:10+00:00" }, { "name": "guzzlehttp/promises", @@ -2420,16 +2353,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", "shasum": "" }, "require": { @@ -2437,9 +2370,9 @@ "php": "^7.1|^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^7.5|^8.5|^9.4", "vimeo/psalm": "^4.3" }, @@ -2473,9 +2406,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2024-03-08T09:58:59+00:00" }, { "name": "jms/metadata", @@ -2543,27 +2476,27 @@ }, { "name": "jms/serializer", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "111451f43abb448ce297361a8ab96a9591e848cd" + "reference": "bf1105358123d7c02ee6cad08ea33ab535a09d5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/111451f43abb448ce297361a8ab96a9591e848cd", - "reference": "111451f43abb448ce297361a8ab96a9591e848cd", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/bf1105358123d7c02ee6cad08ea33ab535a09d5e", + "reference": "bf1105358123d7c02ee6cad08ea33ab535a09d5e", "shasum": "" }, "require": { - "doctrine/annotations": "^1.14 || ^2.0", "doctrine/instantiator": "^1.3.1 || ^2.0", "doctrine/lexer": "^2.0 || ^3.0", "jms/metadata": "^2.6", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^1.20" }, "require-dev": { + "doctrine/annotations": "^1.14 || ^2.0", "doctrine/coding-standard": "^12.0", "doctrine/orm": "^2.14 || ^3.0", "doctrine/persistence": "^2.5.2 || ^3.0", @@ -2573,16 +2506,17 @@ "ocramius/proxy-manager": "^1.0 || ^2.0", "phpbench/phpbench": "^1.0", "phpstan/phpstan": "^1.0.2", - "phpunit/phpunit": "^8.5.21 || ^9.0 || ^10.0", + "phpunit/phpunit": "^9.0 || ^10.0", "psr/container": "^1.0 || ^2.0", - "symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/expression-language": "^3.2 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/filesystem": "^4.2 || ^5.0 || ^6.0 || ^7.0", - "symfony/form": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/translation": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/uid": "^5.1 || ^6.0 || ^7.0", - "symfony/validator": "^3.1.9 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "rector/rector": "^0.19.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/form": "^5.4 || ^6.0 || ^7.0", + "symfony/translation": "^5.4 || ^6.0 || ^7.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0", "twig/twig": "^1.34 || ^2.4 || ^3.0" }, "suggest": { @@ -2627,7 +2561,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/serializer/issues", - "source": "https://github.com/schmittjoh/serializer/tree/3.29.1" + "source": "https://github.com/schmittjoh/serializer/tree/3.30.0" }, "funding": [ { @@ -2635,7 +2569,7 @@ "type": "github" } ], - "time": "2023-12-14T15:25:09+00:00" + "time": "2024-02-24T14:12:14+00:00" }, { "name": "jms/serializer-bundle", @@ -2727,16 +2661,16 @@ }, { "name": "league/commonmark", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5" + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", "shasum": "" }, "require": { @@ -2749,7 +2683,7 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.0", + "commonmark/cmark": "0.30.3", "commonmark/commonmark.js": "0.30.0", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", @@ -2759,10 +2693,10 @@ "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -2829,7 +2763,7 @@ "type": "tidelift" } ], - "time": "2023-08-30T16:55:00+00:00" + "time": "2024-02-02T11:59:32+00:00" }, { "name": "league/config", @@ -2915,16 +2849,16 @@ }, { "name": "league/uri", - "version": "7.4.0", + "version": "7.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5" + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/bf414ba956d902f5d98bf9385fcf63954f09dce5", - "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4", "shasum": "" }, "require": { @@ -2993,7 +2927,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.4.0" + "source": "https://github.com/thephpleague/uri/tree/7.4.1" }, "funding": [ { @@ -3001,20 +2935,20 @@ "type": "github" } ], - "time": "2023-12-01T06:24:25+00:00" + "time": "2024-03-23T07:42:40+00:00" }, { "name": "league/uri-interfaces", - "version": "7.4.0", + "version": "7.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3" + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/bd8c487ec236930f7bbc42b8d374fa882fbba0f3", - "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/8d43ef5c841032c87e2de015972c06f3865ef718", + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718", "shasum": "" }, "require": { @@ -3077,7 +3011,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.1" }, "funding": [ { @@ -3085,20 +3019,20 @@ "type": "github" } ], - "time": "2023-11-24T15:40:42+00:00" + "time": "2024-03-23T07:42:40+00:00" }, { "name": "masterminds/html5", - "version": "2.8.1", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf" + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf", - "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", "shasum": "" }, "require": { @@ -3106,7 +3040,7 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" }, "type": "library", "extra": { @@ -3150,9 +3084,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.8.1" + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" }, - "time": "2023-05-10T11:58:31+00:00" + "time": "2024-03-31T07:05:07+00:00" }, { "name": "mbostock/d3", @@ -3199,16 +3133,16 @@ }, { "name": "monolog/monolog", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448" + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448", - "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", "shasum": "" }, "require": { @@ -3231,7 +3165,7 @@ "phpstan/phpstan": "^1.9", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "^10.1", + "phpunit/phpunit": "^10.5.17", "predis/predis": "^1.1 || ^2", "ruflin/elastica": "^7", "symfony/mailer": "^5.4 || ^6", @@ -3284,7 +3218,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.5.0" + "source": "https://github.com/Seldaek/monolog/tree/3.6.0" }, "funding": [ { @@ -3296,65 +3230,74 @@ "type": "tidelift" } ], - "time": "2023-10-27T15:32:31+00:00" + "time": "2024-04-12T21:02:21+00:00" }, { "name": "nelmio/api-doc-bundle", - "version": "v4.19.2", + "version": "v4.28.0", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "5a5049dd00ce69b7b04c1485f3f41ec58ff7ec3b" + "reference": "684391a5fab4bdfac752560d3483d0f7109448a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/5a5049dd00ce69b7b04c1485f3f41ec58ff7ec3b", - "reference": "5a5049dd00ce69b7b04c1485f3f41ec58ff7ec3b", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/684391a5fab4bdfac752560d3483d0f7109448a5", + "reference": "684391a5fab4bdfac752560d3483d0f7109448a5", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.2", - "phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0", - "psr/cache": "^1.0|^2.0|^3.0", - "psr/container": "^1.0|^2.0", - "psr/log": "^1.0|^2.0|^3.0", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/framework-bundle": "^5.4.24|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/options-resolver": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", - "zircote/swagger-php": "^4.2.15" + "php": ">=7.4", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0", + "phpdocumentor/type-resolver": "^1.8.2", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/config": "^5.4 || ^6.4 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/framework-bundle": "^5.4.24 || ^6.4 || ^7.0", + "symfony/http-foundation": "^5.4 || ^6.4 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/property-info": "^5.4.10 || ^6.4 || ^7.0", + "symfony/routing": "^5.4 || ^6.4 || ^7.0", + "zircote/swagger-php": "^4.6.1" + }, + "conflict": { + "zircote/swagger-php": "4.8.7" }, "require-dev": { - "api-platform/core": "^2.7.0|^3", + "api-platform/core": "^2.7.0 || ^3", "composer/package-versions-deprecated": "1.11.99.1", "doctrine/annotations": "^2.0", - "friendsofsymfony/rest-bundle": "^2.8|^3.0", - "jms/serializer": "^1.14|^3.0", - "jms/serializer-bundle": "^2.3|^3.0|^4.0|^5.0", - "phpunit/phpunit": "^8.5|^9.6", - "sensio/framework-extra-bundle": "^5.4|^6.0", - "symfony/asset": "^5.4|^6.0|^7.0", - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/form": "^5.4|^6.0|^7.0", + "friendsofphp/php-cs-fixer": "^3.52", + "friendsofsymfony/rest-bundle": "^2.8 || ^3.0", + "jms/serializer": "^1.14 || ^3.0", + "jms/serializer-bundle": "^2.3 || ^3.0 || ^4.0 || ^5.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^9.6 || ^10.5", + "symfony/asset": "^5.4 || ^6.4 || ^7.0", + "symfony/browser-kit": "^5.4 || ^6.4 || ^7.0", + "symfony/cache": "^5.4 || ^6.4 || ^7.0", + "symfony/dom-crawler": "^5.4 || ^6.4 || ^7.0", + "symfony/expression-language": "^5.4 || ^6.4 || ^7.0", + "symfony/form": "^5.4 || ^6.4 || ^7.0", "symfony/phpunit-bridge": "^6.4", - "symfony/property-access": "^5.4|^6.0|^7.0", - "symfony/security-csrf": "^5.4|^6.0|^7.0", - "symfony/serializer": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/templating": "^5.4|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", - "symfony/validator": "^5.4|^6.0|^7.0", - "willdurand/hateoas-bundle": "^1.0|^2.0" + "symfony/property-access": "^5.4 || ^6.4 || ^7.0", + "symfony/security-csrf": "^5.4 || ^6.4 || ^7.0", + "symfony/serializer": "^5.4 || ^6.4 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0", + "symfony/templating": "^5.4 || ^6.4 || ^7.0", + "symfony/twig-bundle": "^5.4 || ^6.4 || ^7.0", + "symfony/uid": "^5.4 || ^6.4 || ^7.0", + "symfony/validator": "^5.4 || ^6.4 || ^7.0", + "willdurand/hateoas-bundle": "^1.0 || ^2.0" }, "suggest": { "api-platform/core": "For using an API oriented framework.", @@ -3379,11 +3322,8 @@ }, "autoload": { "psr-4": { - "Nelmio\\ApiDocBundle\\": "" - }, - "exclude-from-classmap": [ - "Tests/" - ] + "Nelmio\\ApiDocBundle\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3395,7 +3335,7 @@ "homepage": "https://github.com/nelmio/NelmioApiDocBundle/contributors" } ], - "description": "Generates documentation for your REST API from annotations", + "description": "Generates documentation for your REST API from annotations and attributes", "keywords": [ "api", "doc", @@ -3404,9 +3344,71 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.19.2" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.28.0" }, - "time": "2024-01-30T09:05:50+00:00" + "time": "2024-06-19T14:37:45+00:00" + }, + { + "name": "nelmio/cors-bundle", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "78fcdb91f76b080a1008133def9c7f613833933d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/78fcdb91f76b080a1008133def9c7f613833933d", + "reference": "78fcdb91f76b080a1008133def9c7f613833933d", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.6", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.4.0" + }, + "time": "2023-11-30T16:41:19+00:00" }, { "name": "nette/schema", @@ -3680,16 +3682,16 @@ }, { "name": "php-http/discovery", - "version": "1.19.2", + "version": "1.19.4", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb" + "reference": "0700efda8d7526335132360167315fdab3aeb599" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", - "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", + "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", + "reference": "0700efda8d7526335132360167315fdab3aeb599", "shasum": "" }, "require": { @@ -3713,7 +3715,8 @@ "php-http/httplug": "^1.0 || ^2.0", "php-http/message-factory": "^1.0", "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", - "symfony/phpunit-bridge": "^6.2" + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" }, "type": "composer-plugin", "extra": { @@ -3752,9 +3755,9 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.19.2" + "source": "https://github.com/php-http/discovery/tree/1.19.4" }, - "time": "2023-11-30T16:49:05+00:00" + "time": "2024-03-29T13:00:05+00:00" }, { "name": "php-http/httplug", @@ -3815,16 +3818,16 @@ }, { "name": "php-http/message", - "version": "1.16.0", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd" + "reference": "5997f3289332c699fa2545c427826272498a2088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/47a14338bf4ebd67d317bf1144253d7db4ab55fd", - "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd", + "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", + "reference": "5997f3289332c699fa2545c427826272498a2088", "shasum": "" }, "require": { @@ -3878,9 +3881,9 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.0" + "source": "https://github.com/php-http/message/tree/1.16.1" }, - "time": "2023-05-17T06:43:38+00:00" + "time": "2024-03-07T13:22:09+00:00" }, { "name": "php-http/message-factory", @@ -3939,16 +3942,16 @@ }, { "name": "php-http/promise", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07" + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/2916a606d3b390f4e9e8e2b8dd68581508be0f07", - "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", "shasum": "" }, "require": { @@ -3985,9 +3988,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.3.0" + "source": "https://github.com/php-http/promise/tree/1.3.1" }, - "time": "2024-01-04T18:49:48+00:00" + "time": "2024-03-15T13:55:21+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4044,28 +4047,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "version": "5.4.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "mockery/mockery": "~1.3.5", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.13" }, "type": "library", "extra": { @@ -4089,33 +4099,33 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2024-05-21T05:55:05+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" + "reference": "153ae662783729388a584b4361f2545e4d841e3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", + "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", "phpstan/phpdoc-parser": "^1.13" }, @@ -4153,22 +4163,22 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" }, - "time": "2024-01-11T11:49:22+00:00" + "time": "2024-02-23T11:10:43+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.25.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -4200,22 +4210,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-01-04T17:06:16+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "promphp/prometheus_client_php", - "version": "v2.9.0", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/PromPHP/prometheus_client_php.git", - "reference": "ad2a85fa52a7b9f6e6d84095cd34d1cb6f0a06bc" + "reference": "a09ea80ec1ec26dd1d4853e9af2a811e898dbfeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PromPHP/prometheus_client_php/zipball/ad2a85fa52a7b9f6e6d84095cd34d1cb6f0a06bc", - "reference": "ad2a85fa52a7b9f6e6d84095cd34d1cb6f0a06bc", + "url": "https://api.github.com/repos/PromPHP/prometheus_client_php/zipball/a09ea80ec1ec26dd1d4853e9af2a811e898dbfeb", + "reference": "a09ea80ec1ec26dd1d4853e9af2a811e898dbfeb", "shasum": "" }, "require": { @@ -4267,9 +4277,9 @@ "description": "Prometheus instrumentation library for PHP applications.", "support": { "issues": "https://github.com/PromPHP/prometheus_client_php/issues", - "source": "https://github.com/PromPHP/prometheus_client_php/tree/v2.9.0" + "source": "https://github.com/PromPHP/prometheus_client_php/tree/v2.10.0" }, - "time": "2023-12-20T06:22:58+00:00" + "time": "2024-02-01T13:28:34+00:00" }, { "name": "psr/cache", @@ -4525,20 +4535,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -4562,7 +4572,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -4574,9 +4584,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -4816,20 +4826,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.5", + "version": "4.7.6", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e" + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", - "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", "ext-json": "*", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" @@ -4892,7 +4902,7 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.5" + "source": "https://github.com/ramsey/uuid/tree/4.7.6" }, "funding": [ { @@ -4904,7 +4914,63 @@ "type": "tidelift" } ], - "time": "2023-11-08T05:53:05+00:00" + "time": "2024-04-27T21:32:50+00:00" + }, + { + "name": "riverline/multipart-parser", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/Riverline/multipart-parser.git", + "reference": "7a9f4646db5181516c61b8e0225a343189beedcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Riverline/multipart-parser/zipball/7a9f4646db5181516c61b8e0225a343189beedcd", + "reference": "7a9f4646db5181516c61b8e0225a343189beedcd", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "laminas/laminas-diactoros": "^1.8.7 || ^2.11.1", + "phpunit/phpunit": "^5.7 || ^9.0", + "psr/http-message": "^1.0", + "symfony/psr-http-message-bridge": "^1.1 || ^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Riverline\\MultiPartParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Romain Cambien", + "email": "romain@cambien.net" + }, + { + "name": "Riverline", + "homepage": "http://www.riverline.fr" + } + ], + "description": "One class library to parse multipart content with encoding and charset support.", + "keywords": [ + "http", + "multipart", + "parser" + ], + "support": { + "issues": "https://github.com/Riverline/multipart-parser/issues", + "source": "https://github.com/Riverline/multipart-parser/tree/2.1.2" + }, + "time": "2024-03-12T16:46:05+00:00" }, { "name": "select2/select2", @@ -5107,16 +5173,16 @@ }, { "name": "sentry/sentry-symfony", - "version": "4.13.2", + "version": "4.14.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-symfony.git", - "reference": "bf049e69863465f2e0ba2555dbb5224641a37d67" + "reference": "001c4cfd8fe93cbb00edaca903ffbfac28259170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/bf049e69863465f2e0ba2555dbb5224641a37d67", - "reference": "bf049e69863465f2e0ba2555dbb5224641a37d67", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/001c4cfd8fe93cbb00edaca903ffbfac28259170", + "reference": "001c4cfd8fe93cbb00edaca903ffbfac28259170", "shasum": "" }, "require": { @@ -5137,8 +5203,8 @@ "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0" }, "require-dev": { - "doctrine/dbal": "^2.13||^3.0", - "doctrine/doctrine-bundle": "^1.12||^2.5", + "doctrine/dbal": "^2.13||^3.3||^4.0", + "doctrine/doctrine-bundle": "^2.6", "friendsofphp/php-cs-fixer": "^2.19||^3.40", "masterminds/html5": "^2.8", "phpstan/extension-installer": "^1.0", @@ -5205,7 +5271,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-symfony/issues", - "source": "https://github.com/getsentry/sentry-symfony/tree/4.13.2" + "source": "https://github.com/getsentry/sentry-symfony/tree/4.14.0" }, "funding": [ { @@ -5217,20 +5283,20 @@ "type": "custom" } ], - "time": "2024-01-11T14:55:45+00:00" + "time": "2024-02-26T09:27:19+00:00" }, { "name": "symfony/asset", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/asset.git", - "reference": "14b1c0fddb64af6ea626af51bb3c47af9fa19cb7" + "reference": "c668aa320e26b7379540368832b9d1dd43d32603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset/zipball/14b1c0fddb64af6ea626af51bb3c47af9fa19cb7", - "reference": "14b1c0fddb64af6ea626af51bb3c47af9fa19cb7", + "url": "https://api.github.com/repos/symfony/asset/zipball/c668aa320e26b7379540368832b9d1dd43d32603", + "reference": "c668aa320e26b7379540368832b9d1dd43d32603", "shasum": "" }, "require": { @@ -5270,7 +5336,7 @@ "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/asset/tree/v6.4.3" + "source": "https://github.com/symfony/asset/tree/v6.4.8" }, "funding": [ { @@ -5286,20 +5352,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/browser-kit", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "495ffa2e6d17e199213f93768efa01af32bbf70e" + "reference": "62ab90b92066ef6cce5e79365625b4b1432464c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/495ffa2e6d17e199213f93768efa01af32bbf70e", - "reference": "495ffa2e6d17e199213f93768efa01af32bbf70e", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/62ab90b92066ef6cce5e79365625b4b1432464c8", + "reference": "62ab90b92066ef6cce5e79365625b4b1432464c8", "shasum": "" }, "require": { @@ -5338,7 +5404,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v6.4.3" + "source": "https://github.com/symfony/browser-kit/tree/v6.4.8" }, "funding": [ { @@ -5354,20 +5420,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/cache", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "49f8cdee544a621a621cd21b6cda32a38926d310" + "reference": "287142df5579ce223c485b3872df3efae8390984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/49f8cdee544a621a621cd21b6cda32a38926d310", - "reference": "49f8cdee544a621a621cd21b6cda32a38926d310", + "url": "https://api.github.com/repos/symfony/cache/zipball/287142df5579ce223c485b3872df3efae8390984", + "reference": "287142df5579ce223c485b3872df3efae8390984", "shasum": "" }, "require": { @@ -5434,7 +5500,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.3" + "source": "https://github.com/symfony/cache/tree/v6.4.8" }, "funding": [ { @@ -5450,20 +5516,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "1d74b127da04ffa87aa940abe15446fa89653778" + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778", - "reference": "1d74b127da04ffa87aa940abe15446fa89653778", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/df6a1a44c890faded49a5fca33c2d5c5fd3c2197", + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197", "shasum": "" }, "require": { @@ -5473,7 +5539,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5510,7 +5576,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.0" }, "funding": [ { @@ -5526,20 +5592,20 @@ "type": "tidelift" } ], - "time": "2023-09-25T12:52:38+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/clock", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "f48770105c544001da00b8d745873a628e0de198" + "reference": "7a4840efd17135cbd547e41ec49fb910ed4f8b98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/f48770105c544001da00b8d745873a628e0de198", - "reference": "f48770105c544001da00b8d745873a628e0de198", + "url": "https://api.github.com/repos/symfony/clock/zipball/7a4840efd17135cbd547e41ec49fb910ed4f8b98", + "reference": "7a4840efd17135cbd547e41ec49fb910ed4f8b98", "shasum": "" }, "require": { @@ -5584,7 +5650,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v6.4.3" + "source": "https://github.com/symfony/clock/tree/v6.4.8" }, "funding": [ { @@ -5600,20 +5666,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:51:39+00:00" }, { "name": "symfony/config", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "206482ff3ed450495b1d5b7bad1bc3a852def96f" + "reference": "12e7e52515ce37191b193cf3365903c4f3951e35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/206482ff3ed450495b1d5b7bad1bc3a852def96f", - "reference": "206482ff3ed450495b1d5b7bad1bc3a852def96f", + "url": "https://api.github.com/repos/symfony/config/zipball/12e7e52515ce37191b193cf3365903c4f3951e35", + "reference": "12e7e52515ce37191b193cf3365903c4f3951e35", "shasum": "" }, "require": { @@ -5659,7 +5725,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.3" + "source": "https://github.com/symfony/config/tree/v6.4.8" }, "funding": [ { @@ -5675,20 +5741,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T13:26:27+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/console", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e" + "reference": "be5854cee0e8c7b110f00d695d11debdfa1a2a91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", - "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", + "url": "https://api.github.com/repos/symfony/console/zipball/be5854cee0e8c7b110f00d695d11debdfa1a2a91", + "reference": "be5854cee0e8c7b110f00d695d11debdfa1a2a91", "shasum": "" }, "require": { @@ -5753,7 +5819,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.3" + "source": "https://github.com/symfony/console/tree/v6.4.8" }, "funding": [ { @@ -5769,20 +5835,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/css-selector", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229" + "reference": "4b61b02fe15db48e3687ce1c45ea385d1780fe08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/ee0f7ed5cf298cc019431bb3b3977ebc52b86229", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/4b61b02fe15db48e3687ce1c45ea385d1780fe08", + "reference": "4b61b02fe15db48e3687ce1c45ea385d1780fe08", "shasum": "" }, "require": { @@ -5818,7 +5884,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.4.3" + "source": "https://github.com/symfony/css-selector/tree/v6.4.8" }, "funding": [ { @@ -5834,20 +5900,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "6871811c5a5c5e180244ddb689746446db02c05b" + "reference": "d3b618176e8c3a9e5772151c51eba0c52a0c771c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6871811c5a5c5e180244ddb689746446db02c05b", - "reference": "6871811c5a5c5e180244ddb689746446db02c05b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d3b618176e8c3a9e5772151c51eba0c52a0c771c", + "reference": "d3b618176e8c3a9e5772151c51eba0c52a0c771c", "shasum": "" }, "require": { @@ -5899,7 +5965,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.3" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.8" }, "funding": [ { @@ -5915,20 +5981,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -5937,7 +6003,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5966,7 +6032,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -5982,20 +6048,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "9c9a44bb06337dadeb9db1a8b202f15cca804353" + "reference": "afbf291ccaf595c8ff6f4ed3943aa0ea479e4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9c9a44bb06337dadeb9db1a8b202f15cca804353", - "reference": "9c9a44bb06337dadeb9db1a8b202f15cca804353", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/afbf291ccaf595c8ff6f4ed3943aa0ea479e4d04", + "reference": "afbf291ccaf595c8ff6f4ed3943aa0ea479e4d04", "shasum": "" }, "require": { @@ -6013,7 +6079,7 @@ "doctrine/orm": "<2.15", "symfony/cache": "<5.4", "symfony/dependency-injection": "<6.2", - "symfony/form": "<5.4.21|>=6,<6.2.7", + "symfony/form": "<5.4.38|>=6,<6.4.6|>=7,<7.0.6", "symfony/http-foundation": "<6.3", "symfony/http-kernel": "<6.2", "symfony/lock": "<6.3", @@ -6034,7 +6100,7 @@ "symfony/dependency-injection": "^6.2|^7.0", "symfony/doctrine-messenger": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/form": "^5.4.21|^6.2.7|^7.0", + "symfony/form": "^5.4.38|^6.4.6|^7.0.6", "symfony/http-kernel": "^6.3|^7.0", "symfony/lock": "^6.3|^7.0", "symfony/messenger": "^5.4|^6.0|^7.0", @@ -6074,7 +6140,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.8" }, "funding": [ { @@ -6090,20 +6156,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T11:24:52+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/dom-crawler", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "6db31849011fefe091e94d0bb10cba26f7919894" + "reference": "105b56a0305d219349edeb60a800082eca864e4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/6db31849011fefe091e94d0bb10cba26f7919894", - "reference": "6db31849011fefe091e94d0bb10cba26f7919894", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/105b56a0305d219349edeb60a800082eca864e4b", + "reference": "105b56a0305d219349edeb60a800082eca864e4b", "shasum": "" }, "require": { @@ -6141,7 +6207,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.3" + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.8" }, "funding": [ { @@ -6157,20 +6223,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/dotenv", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "3cb7ca997124760ed1389d5341806247670f4ef8" + "reference": "55aefa0029adff89ecffdb560820e945c7983f06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/3cb7ca997124760ed1389d5341806247670f4ef8", - "reference": "3cb7ca997124760ed1389d5341806247670f4ef8", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/55aefa0029adff89ecffdb560820e945c7983f06", + "reference": "55aefa0029adff89ecffdb560820e945c7983f06", "shasum": "" }, "require": { @@ -6215,7 +6281,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.3" + "source": "https://github.com/symfony/dotenv/tree/v6.4.8" }, "funding": [ { @@ -6231,20 +6297,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/error-handler", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "6dc3c76a278b77f01d864a6005d640822c6f26a6" + "reference": "ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/6dc3c76a278b77f01d864a6005d640822c6f26a6", - "reference": "6dc3c76a278b77f01d864a6005d640822c6f26a6", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc", + "reference": "ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc", "shasum": "" }, "require": { @@ -6290,7 +6356,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.3" + "source": "https://github.com/symfony/error-handler/tree/v6.4.8" }, "funding": [ { @@ -6306,20 +6372,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:40:36+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef" + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", "shasum": "" }, "require": { @@ -6370,7 +6436,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" }, "funding": [ { @@ -6386,20 +6452,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { @@ -6409,7 +6475,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -6446,7 +6512,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -6462,20 +6528,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/expression-language", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "b4a4ae33fbb33a99d23c5698faaecadb76ad0fe4" + "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/b4a4ae33fbb33a99d23c5698faaecadb76ad0fe4", - "reference": "b4a4ae33fbb33a99d23c5698faaecadb76ad0fe4", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", + "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", "shasum": "" }, "require": { @@ -6510,7 +6576,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v6.4.3" + "source": "https://github.com/symfony/expression-language/tree/v6.4.8" }, "funding": [ { @@ -6526,20 +6592,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" + "reference": "4d37529150e7081c51b3c5d5718c55a04a9503f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/4d37529150e7081c51b3c5d5718c55a04a9503f3", + "reference": "4d37529150e7081c51b3c5d5718c55a04a9503f3", "shasum": "" }, "require": { @@ -6547,6 +6613,9 @@ "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" + }, "type": "library", "autoload": { "psr-4": { @@ -6573,7 +6642,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.3" + "source": "https://github.com/symfony/filesystem/tree/v6.4.8" }, "funding": [ { @@ -6589,20 +6658,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/finder", - "version": "v6.4.0", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + "reference": "3ef977a43883215d560a2cecb82ec8e62131471c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "url": "https://api.github.com/repos/symfony/finder/zipball/3ef977a43883215d560a2cecb82ec8e62131471c", + "reference": "3ef977a43883215d560a2cecb82ec8e62131471c", "shasum": "" }, "require": { @@ -6637,7 +6706,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.0" + "source": "https://github.com/symfony/finder/tree/v6.4.8" }, "funding": [ { @@ -6653,20 +6722,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T17:30:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/flex", - "version": "v2.4.3", + "version": "v2.4.5", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1" + "reference": "b0a405f40614c9f584b489d54f91091817b0e26e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1", - "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1", + "url": "https://api.github.com/repos/symfony/flex/zipball/b0a405f40614c9f584b489d54f91091817b0e26e", + "reference": "b0a405f40614c9f584b489d54f91091817b0e26e", "shasum": "" }, "require": { @@ -6702,7 +6771,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.4.3" + "source": "https://github.com/symfony/flex/tree/v2.4.5" }, "funding": [ { @@ -6718,20 +6787,20 @@ "type": "tidelift" } ], - "time": "2024-01-02T11:08:32+00:00" + "time": "2024-03-02T08:16:47+00:00" }, { "name": "symfony/form", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "dabf7e9375550aada8916ced1736d01c2e3debff" + "reference": "196ebc738e59bec2bbf1f49c24cc221b47f77f5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/dabf7e9375550aada8916ced1736d01c2e3debff", - "reference": "dabf7e9375550aada8916ced1736d01c2e3debff", + "url": "https://api.github.com/repos/symfony/form/zipball/196ebc738e59bec2bbf1f49c24cc221b47f77f5d", + "reference": "196ebc738e59bec2bbf1f49c24cc221b47f77f5d", "shasum": "" }, "require": { @@ -6799,7 +6868,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v6.4.3" + "source": "https://github.com/symfony/form/tree/v6.4.8" }, "funding": [ { @@ -6815,20 +6884,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "fb413ac4483803954411966a39f3a9204835848e" + "reference": "7c7739f87f1a8be1c2f5e7d28addfe763a917acb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/fb413ac4483803954411966a39f3a9204835848e", - "reference": "fb413ac4483803954411966a39f3a9204835848e", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/7c7739f87f1a8be1c2f5e7d28addfe763a917acb", + "reference": "7c7739f87f1a8be1c2f5e7d28addfe763a917acb", "shasum": "" }, "require": { @@ -6867,7 +6936,7 @@ "symfony/mime": "<6.4", "symfony/property-access": "<5.4", "symfony/property-info": "<5.4", - "symfony/scheduler": "<6.4.3|>=7.0.0,<7.0.3", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", "symfony/security-core": "<5.4", "symfony/security-csrf": "<5.4", "symfony/serializer": "<6.4", @@ -6906,7 +6975,7 @@ "symfony/process": "^5.4|^6.0|^7.0", "symfony/property-info": "^5.4|^6.0|^7.0", "symfony/rate-limiter": "^5.4|^6.0|^7.0", - "symfony/scheduler": "^6.4.3|^7.0.3", + "symfony/scheduler": "^6.4.4|^7.0.4", "symfony/security-bundle": "^5.4|^6.0|^7.0", "symfony/semaphore": "^5.4|^6.0|^7.0", "symfony/serializer": "^6.4|^7.0", @@ -6919,7 +6988,7 @@ "symfony/web-link": "^5.4|^6.0|^7.0", "symfony/workflow": "^6.4|^7.0", "symfony/yaml": "^5.4|^6.0|^7.0", - "twig/twig": "^2.10|^3.0" + "twig/twig": "^2.10|^3.0.4" }, "type": "symfony-bundle", "autoload": { @@ -6947,7 +7016,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.8" }, "funding": [ { @@ -6963,20 +7032,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:02:55+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "116335ab09e10b05405f01d8afd31ccc3832b08b" + "reference": "9de29a710320ee802374e479169c5a82f9ee7854" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/116335ab09e10b05405f01d8afd31ccc3832b08b", - "reference": "116335ab09e10b05405f01d8afd31ccc3832b08b", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/9de29a710320ee802374e479169c5a82f9ee7854", + "reference": "9de29a710320ee802374e479169c5a82f9ee7854", "shasum": "" }, "require": { @@ -7016,7 +7085,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v6.4.3" + "source": "https://github.com/symfony/html-sanitizer/tree/v6.4.8" }, "funding": [ { @@ -7032,27 +7101,27 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:51:39+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86" + "reference": "61faba993e620fc22d4f0ab3b6bcf8fbb0d44b05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/a9034bc119fab8238f76cf49c770f3135f3ead86", - "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86", + "url": "https://api.github.com/repos/symfony/http-client/zipball/61faba993e620fc22d4f0ab3b6bcf8fbb0d44b05", + "reference": "61faba993e620fc22d4f0ab3b6bcf8fbb0d44b05", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3", + "symfony/http-client-contracts": "^3.4.1", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -7070,7 +7139,7 @@ "amphp/http-client": "^4.2.1", "amphp/http-tunnel": "^1.0", "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4", + "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", @@ -7109,7 +7178,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.3" + "source": "https://github.com/symfony/http-client/tree/v6.4.8" }, "funding": [ { @@ -7125,20 +7194,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:01:07+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "1ee70e699b41909c209a0c930f11034b93578654" + "reference": "20414d96f391677bf80078aa55baece78b82647d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/1ee70e699b41909c209a0c930f11034b93578654", - "reference": "1ee70e699b41909c209a0c930f11034b93578654", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", "shasum": "" }, "require": { @@ -7147,7 +7216,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -7187,7 +7256,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" }, "funding": [ { @@ -7203,20 +7272,20 @@ "type": "tidelift" } ], - "time": "2023-07-30T20:28:31+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "5677bdf7cade4619cb17fc9e1e7b31ec392244a9" + "reference": "27de8cc95e11db7a50b027e71caaab9024545947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5677bdf7cade4619cb17fc9e1e7b31ec392244a9", - "reference": "5677bdf7cade4619cb17fc9e1e7b31ec392244a9", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/27de8cc95e11db7a50b027e71caaab9024545947", + "reference": "27de8cc95e11db7a50b027e71caaab9024545947", "shasum": "" }, "require": { @@ -7264,7 +7333,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.3" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.8" }, "funding": [ { @@ -7280,20 +7349,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9c6ec4e543044f7568a53a76ab1484ecd30637a2" + "reference": "6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9c6ec4e543044f7568a53a76ab1484ecd30637a2", - "reference": "9c6ec4e543044f7568a53a76ab1484ecd30637a2", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1", + "reference": "6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1", "shasum": "" }, "require": { @@ -7342,12 +7411,13 @@ "symfony/process": "^5.4|^6.0|^7.0", "symfony/property-access": "^5.4.5|^6.0.5|^7.0", "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.3|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", "symfony/stopwatch": "^5.4|^6.0|^7.0", "symfony/translation": "^5.4|^6.0|^7.0", "symfony/translation-contracts": "^2.5|^3", "symfony/uid": "^5.4|^6.0|^7.0", "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", "symfony/var-exporter": "^6.2|^7.0", "twig/twig": "^2.13|^3.0.4" }, @@ -7377,7 +7447,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.3" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.8" }, "funding": [ { @@ -7393,20 +7463,20 @@ "type": "tidelift" } ], - "time": "2024-01-31T07:21:29+00:00" + "time": "2024-06-02T16:06:25+00:00" }, { "name": "symfony/intl", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "2628ded562ca132ed7cdea72f5ec6aaf65d94414" + "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/2628ded562ca132ed7cdea72f5ec6aaf65d94414", - "reference": "2628ded562ca132ed7cdea72f5ec6aaf65d94414", + "url": "https://api.github.com/repos/symfony/intl/zipball/50265cdcf5a44bec3fcf487b5d0015aece91d1eb", + "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb", "shasum": "" }, "require": { @@ -7423,7 +7493,8 @@ "Symfony\\Component\\Intl\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/data/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7459,7 +7530,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.4.3" + "source": "https://github.com/symfony/intl/tree/v6.4.8" }, "funding": [ { @@ -7475,20 +7546,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/mime", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "5017e0a9398c77090b7694be46f20eb796262a34" + "reference": "618597ab8b78ac86d1c75a9d0b35540cda074f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/5017e0a9398c77090b7694be46f20eb796262a34", - "reference": "5017e0a9398c77090b7694be46f20eb796262a34", + "url": "https://api.github.com/repos/symfony/mime/zipball/618597ab8b78ac86d1c75a9d0b35540cda074f33", + "reference": "618597ab8b78ac86d1c75a9d0b35540cda074f33", "shasum": "" }, "require": { @@ -7509,6 +7580,7 @@ "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", "symfony/property-access": "^5.4|^6.0|^7.0", "symfony/property-info": "^5.4|^6.0|^7.0", "symfony/serializer": "^6.3.2|^7.0" @@ -7543,7 +7615,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.3" + "source": "https://github.com/symfony/mime/tree/v6.4.8" }, "funding": [ { @@ -7559,20 +7631,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-06-01T07:50:16+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "1e1ec293f15dcc815146637ee9ee8a7f43642fa1" + "reference": "0fbee64913b1c595e7650a1919ba3edba8d49ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/1e1ec293f15dcc815146637ee9ee8a7f43642fa1", - "reference": "1e1ec293f15dcc815146637ee9ee8a7f43642fa1", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/0fbee64913b1c595e7650a1919ba3edba8d49ea7", + "reference": "0fbee64913b1c595e7650a1919ba3edba8d49ea7", "shasum": "" }, "require": { @@ -7585,7 +7657,7 @@ "conflict": { "symfony/console": "<5.4", "symfony/http-foundation": "<5.4", - "symfony/security-core": "<6.0" + "symfony/security-core": "<5.4" }, "require-dev": { "symfony/console": "^5.4|^6.0|^7.0", @@ -7593,7 +7665,7 @@ "symfony/mailer": "^5.4|^6.0|^7.0", "symfony/messenger": "^5.4|^6.0|^7.0", "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/security-core": "^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "symfony-bridge", @@ -7622,7 +7694,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.8" }, "funding": [ { @@ -7638,7 +7710,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:01:07+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/monolog-bundle", @@ -7723,16 +7795,16 @@ }, { "name": "symfony/options-resolver", - "version": "v6.4.0", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", "shasum": "" }, "require": { @@ -7770,7 +7842,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.8" }, "funding": [ { @@ -7786,20 +7858,20 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:16:24+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/password-hasher", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/password-hasher.git", - "reference": "5189cdfe89a9acb56cc6d6d7a5233bfb180c7268" + "reference": "90ebbe946e5d64a5fad9ac9427e335045cf2bd31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/password-hasher/zipball/5189cdfe89a9acb56cc6d6d7a5233bfb180c7268", - "reference": "5189cdfe89a9acb56cc6d6d7a5233bfb180c7268", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/90ebbe946e5d64a5fad9ac9427e335045cf2bd31", + "reference": "90ebbe946e5d64a5fad9ac9427e335045cf2bd31", "shasum": "" }, "require": { @@ -7842,7 +7914,7 @@ "password" ], "support": { - "source": "https://github.com/symfony/password-hasher/tree/v6.4.3" + "source": "https://github.com/symfony/password-hasher/tree/v6.4.8" }, "funding": [ { @@ -7858,20 +7930,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -7882,9 +7954,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -7923,7 +7992,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -7939,20 +8008,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "e46b4da57951a16053cd751f63f4a24292788157" + "reference": "e76343c631b453088e2260ac41dfebe21954de81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e46b4da57951a16053cd751f63f4a24292788157", - "reference": "e46b4da57951a16053cd751f63f4a24292788157", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e76343c631b453088e2260ac41dfebe21954de81", + "reference": "e76343c631b453088e2260ac41dfebe21954de81", "shasum": "" }, "require": { @@ -7963,9 +8032,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8010,7 +8076,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.30.0" }, "funding": [ { @@ -8026,20 +8092,20 @@ "type": "tidelift" } ], - "time": "2023-03-21T17:27:24+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", "shasum": "" }, "require": { @@ -8052,9 +8118,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8097,7 +8160,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0" }, "funding": [ { @@ -8113,20 +8176,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:30:37+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -8137,9 +8200,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8181,7 +8241,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -8197,20 +8257,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -8224,9 +8284,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8264,7 +8321,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -8280,20 +8337,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + "reference": "10112722600777e02d2745716b70c5db4ca70442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442", + "reference": "10112722600777e02d2745716b70c5db4ca70442", "shasum": "" }, "require": { @@ -8301,9 +8358,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8340,7 +8394,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0" }, "funding": [ { @@ -8356,20 +8410,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -8377,9 +8431,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8423,7 +8474,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -8439,31 +8490,27 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=7.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8503,7 +8550,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" }, "funding": [ { @@ -8519,20 +8566,20 @@ "type": "tidelift" } ], - "time": "2023-08-16T06:22:46+00:00" + "time": "2024-06-19T12:35:24+00:00" }, { "name": "symfony/property-access", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "dd22c9247a16c712bfa128b3c90ebdd505102778" + "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/dd22c9247a16c712bfa128b3c90ebdd505102778", - "reference": "dd22c9247a16c712bfa128b3c90ebdd505102778", + "url": "https://api.github.com/repos/symfony/property-access/zipball/e4d9b00983612f9c0013ca37c61affdba2dd975a", + "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a", "shasum": "" }, "require": { @@ -8580,7 +8627,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.4.3" + "source": "https://github.com/symfony/property-access/tree/v6.4.8" }, "funding": [ { @@ -8596,20 +8643,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/property-info", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "e96d740ab5ac39aa530c8eaa0720ea8169118e26" + "reference": "7f544bc6ceb1a6a2283c7af8e8621262c43b7ede" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/e96d740ab5ac39aa530c8eaa0720ea8169118e26", - "reference": "e96d740ab5ac39aa530c8eaa0720ea8169118e26", + "url": "https://api.github.com/repos/symfony/property-info/zipball/7f544bc6ceb1a6a2283c7af8e8621262c43b7ede", + "reference": "7f544bc6ceb1a6a2283c7af8e8621262c43b7ede", "shasum": "" }, "require": { @@ -8663,7 +8710,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v6.4.3" + "source": "https://github.com/symfony/property-info/tree/v6.4.8" }, "funding": [ { @@ -8679,20 +8726,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "49cfb0223ec64379f7154214dcc1f7c46f3c7a47" + "reference": "23a162bd446b93948a2c2f6909d80ad06195be10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/49cfb0223ec64379f7154214dcc1f7c46f3c7a47", - "reference": "49cfb0223ec64379f7154214dcc1f7c46f3c7a47", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/23a162bd446b93948a2c2f6909d80ad06195be10", + "reference": "23a162bd446b93948a2c2f6909d80ad06195be10", "shasum": "" }, "require": { @@ -8746,7 +8793,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.8" }, "funding": [ { @@ -8762,20 +8809,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:51:39+00:00" }, { "name": "symfony/routing", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "3b2957ad54902f0f544df83e3d58b38d7e8e5842" + "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/3b2957ad54902f0f544df83e3d58b38d7e8e5842", - "reference": "3b2957ad54902f0f544df83e3d58b38d7e8e5842", + "url": "https://api.github.com/repos/symfony/routing/zipball/8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", + "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", "shasum": "" }, "require": { @@ -8829,7 +8876,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.3" + "source": "https://github.com/symfony/routing/tree/v6.4.8" }, "funding": [ { @@ -8845,20 +8892,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T13:55:02+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "5682281d26366cd3bf0648cec69de0e62cca7fa0" + "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/5682281d26366cd3bf0648cec69de0e62cca7fa0", - "reference": "5682281d26366cd3bf0648cec69de0e62cca7fa0", + "url": "https://api.github.com/repos/symfony/runtime/zipball/b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", + "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", "shasum": "" }, "require": { @@ -8908,7 +8955,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.3" + "source": "https://github.com/symfony/runtime/tree/v6.4.8" }, "funding": [ { @@ -8924,20 +8971,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "a53a9e1f6695447ce613aa8c9c698cfd012bd2aa" + "reference": "dfb286069b0332e1f1c21962133d17c0fbc1e5e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/a53a9e1f6695447ce613aa8c9c698cfd012bd2aa", - "reference": "a53a9e1f6695447ce613aa8c9c698cfd012bd2aa", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/dfb286069b0332e1f1c21962133d17c0fbc1e5e7", + "reference": "dfb286069b0332e1f1c21962133d17c0fbc1e5e7", "shasum": "" }, "require": { @@ -9020,7 +9067,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.8" }, "funding": [ { @@ -9036,20 +9083,20 @@ "type": "tidelift" } ], - "time": "2024-01-28T15:49:46+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-core", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "bb10f630cf5b1819ff80aa3ad57a09c61268fc48" + "reference": "5fc7850ada5e8e03d78c1739c82c64d5e2f7d495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/bb10f630cf5b1819ff80aa3ad57a09c61268fc48", - "reference": "bb10f630cf5b1819ff80aa3ad57a09c61268fc48", + "url": "https://api.github.com/repos/symfony/security-core/zipball/5fc7850ada5e8e03d78c1739c82c64d5e2f7d495", + "reference": "5fc7850ada5e8e03d78c1739c82c64d5e2f7d495", "shasum": "" }, "require": { @@ -9106,7 +9153,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.3" + "source": "https://github.com/symfony/security-core/tree/v6.4.8" }, "funding": [ { @@ -9122,20 +9169,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-csrf", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-csrf.git", - "reference": "e10257dd26f965d75e96bbfc27e46efd943f3010" + "reference": "f46ab02b76311087873257071559edcaf6d7ab99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-csrf/zipball/e10257dd26f965d75e96bbfc27e46efd943f3010", - "reference": "e10257dd26f965d75e96bbfc27e46efd943f3010", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/f46ab02b76311087873257071559edcaf6d7ab99", + "reference": "f46ab02b76311087873257071559edcaf6d7ab99", "shasum": "" }, "require": { @@ -9174,7 +9221,7 @@ "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-csrf/tree/v6.4.3" + "source": "https://github.com/symfony/security-csrf/tree/v6.4.8" }, "funding": [ { @@ -9190,20 +9237,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-http", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "d1962d08e02d620dccbaa28192498642500b5043" + "reference": "fb82ddec887dc67f3bcf4d6df3cb8efd529be104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/d1962d08e02d620dccbaa28192498642500b5043", - "reference": "d1962d08e02d620dccbaa28192498642500b5043", + "url": "https://api.github.com/repos/symfony/security-http/zipball/fb82ddec887dc67f3bcf4d6df3cb8efd529be104", + "reference": "fb82ddec887dc67f3bcf4d6df3cb8efd529be104", "shasum": "" }, "require": { @@ -9262,7 +9309,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.4.3" + "source": "https://github.com/symfony/security-http/tree/v6.4.8" }, "funding": [ { @@ -9278,20 +9325,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "51a06ee93c4d5ab5b9edaa0635d8b83953e3c14d" + "reference": "d6eda9966a3e5d1823c1cedf41bf98f8ed969d7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/51a06ee93c4d5ab5b9edaa0635d8b83953e3c14d", - "reference": "51a06ee93c4d5ab5b9edaa0635d8b83953e3c14d", + "url": "https://api.github.com/repos/symfony/serializer/zipball/d6eda9966a3e5d1823c1cedf41bf98f8ed969d7c", + "reference": "d6eda9966a3e5d1823c1cedf41bf98f8ed969d7c", "shasum": "" }, "require": { @@ -9360,7 +9407,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.3" + "source": "https://github.com/symfony/serializer/tree/v6.4.8" }, "funding": [ { @@ -9376,25 +9423,26 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -9402,7 +9450,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -9442,7 +9490,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -9458,20 +9506,20 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1" + "reference": "63e069eb616049632cde9674c46957819454b8aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/416596166641f1f728b0a64f5b9dd07cceb410c1", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/63e069eb616049632cde9674c46957819454b8aa", + "reference": "63e069eb616049632cde9674c46957819454b8aa", "shasum": "" }, "require": { @@ -9504,7 +9552,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.3" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.8" }, "funding": [ { @@ -9520,20 +9568,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:35:58+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/string", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "7a14736fb179876575464e4658fce0c304e8c15b" + "reference": "a147c0f826c4a1f3afb763ab8e009e37c877a44d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/7a14736fb179876575464e4658fce0c304e8c15b", - "reference": "7a14736fb179876575464e4658fce0c304e8c15b", + "url": "https://api.github.com/repos/symfony/string/zipball/a147c0f826c4a1f3afb763ab8e009e37c877a44d", + "reference": "a147c0f826c4a1f3afb763ab8e009e37c877a44d", "shasum": "" }, "require": { @@ -9590,7 +9638,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.3" + "source": "https://github.com/symfony/string/tree/v6.4.8" }, "funding": [ { @@ -9606,20 +9654,20 @@ "type": "tidelift" } ], - "time": "2024-01-25T09:26:29+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "06450585bf65e978026bda220cdebca3f867fde7" + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/06450585bf65e978026bda220cdebca3f867fde7", - "reference": "06450585bf65e978026bda220cdebca3f867fde7", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", "shasum": "" }, "require": { @@ -9628,7 +9676,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -9668,7 +9716,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" }, "funding": [ { @@ -9684,20 +9732,20 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/twig-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "bf6b411a5d9a0ce6ea43cca0fcf5f05f5196a957" + "reference": "57de1b7d7499053a2c5beb9344751e8bfd332649" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/bf6b411a5d9a0ce6ea43cca0fcf5f05f5196a957", - "reference": "bf6b411a5d9a0ce6ea43cca0fcf5f05f5196a957", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/57de1b7d7499053a2c5beb9344751e8bfd332649", + "reference": "57de1b7d7499053a2c5beb9344751e8bfd332649", "shasum": "" }, "require": { @@ -9777,7 +9825,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/twig-bridge/tree/v6.4.8" }, "funding": [ { @@ -9793,20 +9841,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/twig-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "2e63e50de2ade430191af0b5d21bfd6526fe3709" + "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/2e63e50de2ade430191af0b5d21bfd6526fe3709", - "reference": "2e63e50de2ade430191af0b5d21bfd6526fe3709", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", + "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", "shasum": "" }, "require": { @@ -9861,7 +9909,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/twig-bundle/tree/v6.4.8" }, "funding": [ { @@ -9877,20 +9925,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/validator", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "9c1d8bb4edce5304fcefca7923741085f1ca5b60" + "reference": "dab2781371d54c86f6b25623ab16abb2dde2870c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/9c1d8bb4edce5304fcefca7923741085f1ca5b60", - "reference": "9c1d8bb4edce5304fcefca7923741085f1ca5b60", + "url": "https://api.github.com/repos/symfony/validator/zipball/dab2781371d54c86f6b25623ab16abb2dde2870c", + "reference": "dab2781371d54c86f6b25623ab16abb2dde2870c", "shasum": "" }, "require": { @@ -9937,7 +9985,8 @@ "Symfony\\Component\\Validator\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -9957,7 +10006,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.3" + "source": "https://github.com/symfony/validator/tree/v6.4.8" }, "funding": [ { @@ -9973,20 +10022,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:01:07+00:00" + "time": "2024-06-02T15:48:50+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0435a08f69125535336177c29d56af3abc1f69da" + "reference": "ad23ca4312395f0a8a8633c831ef4c4ee542ed25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0435a08f69125535336177c29d56af3abc1f69da", - "reference": "0435a08f69125535336177c29d56af3abc1f69da", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ad23ca4312395f0a8a8633c831ef4c4ee542ed25", + "reference": "ad23ca4312395f0a8a8633c831ef4c4ee542ed25", "shasum": "" }, "require": { @@ -10042,7 +10091,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.3" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.8" }, "funding": [ { @@ -10058,20 +10107,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:53:30+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "a8c12b5448a5ac685347f5eeb2abf6a571ec16b8" + "reference": "792ca836f99b340f2e9ca9497c7953948c49a504" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/a8c12b5448a5ac685347f5eeb2abf6a571ec16b8", - "reference": "a8c12b5448a5ac685347f5eeb2abf6a571ec16b8", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/792ca836f99b340f2e9ca9497c7953948c49a504", + "reference": "792ca836f99b340f2e9ca9497c7953948c49a504", "shasum": "" }, "require": { @@ -10079,6 +10128,8 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", @@ -10117,7 +10168,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.3" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.8" }, "funding": [ { @@ -10133,20 +10184,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "e78f98da7b4f842bab89368d53c962f5b2f9e49c" + "reference": "bcc806d1360991de3bf78ac5ca0202db85de9bfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/e78f98da7b4f842bab89368d53c962f5b2f9e49c", - "reference": "e78f98da7b4f842bab89368d53c962f5b2f9e49c", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/bcc806d1360991de3bf78ac5ca0202db85de9bfc", + "reference": "bcc806d1360991de3bf78ac5ca0202db85de9bfc", "shasum": "" }, "require": { @@ -10199,7 +10250,7 @@ "dev" ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.8" }, "funding": [ { @@ -10215,20 +10266,20 @@ "type": "tidelift" } ], - "time": "2024-01-28T15:49:46+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d75715985f0f94f978e3a8fa42533e10db921b90" + "reference": "52903de178d542850f6f341ba92995d3d63e60c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d75715985f0f94f978e3a8fa42533e10db921b90", - "reference": "d75715985f0f94f978e3a8fa42533e10db921b90", + "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9", + "reference": "52903de178d542850f6f341ba92995d3d63e60c9", "shasum": "" }, "require": { @@ -10271,7 +10322,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.3" + "source": "https://github.com/symfony/yaml/tree/v6.4.8" }, "funding": [ { @@ -10287,20 +10338,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "twbs/bootstrap", - "version": "v5.3.2", + "version": "v5.3.3", "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "344e912d04b5b6a04482113eff20ab416ff01048" + "reference": "6e1f75f420f68e1d52733b8e407fc7c3766c9dba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/344e912d04b5b6a04482113eff20ab416ff01048", - "reference": "344e912d04b5b6a04482113eff20ab416ff01048", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/6e1f75f420f68e1d52733b8e407fc7c3766c9dba", + "reference": "6e1f75f420f68e1d52733b8e407fc7c3766c9dba", "shasum": "" }, "replace": { @@ -10335,40 +10386,40 @@ ], "support": { "issues": "https://github.com/twbs/bootstrap/issues", - "source": "https://github.com/twbs/bootstrap/tree/v5.3.2" + "source": "https://github.com/twbs/bootstrap/tree/v5.3.3" }, - "time": "2023-09-14T14:19:27+00:00" + "time": "2024-02-20T15:14:29+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.8.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "32807183753de0388c8e59f7ac2d13bb47311140" + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/32807183753de0388c8e59f7ac2d13bb47311140", - "reference": "32807183753de0388c8e59f7ac2d13bb47311140", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/cdc6e23aeb7f4953c1039568c3439aab60c56454", + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/framework-bundle": "^5.4|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", "twig/twig": "^3.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", - "twig/cssinliner-extra": "^2.12|^3.0", - "twig/html-extra": "^2.12|^3.0", - "twig/inky-extra": "^2.12|^3.0", - "twig/intl-extra": "^2.12|^3.0", - "twig/markdown-extra": "^2.12|^3.0", - "twig/string-extra": "^2.12|^3.0" + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" }, "type": "symfony-bundle", "autoload": { @@ -10399,7 +10450,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.8.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.10.0" }, "funding": [ { @@ -10411,24 +10462,25 @@ "type": "tidelift" } ], - "time": "2023-11-21T14:02:01+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/markdown-extra", - "version": "v3.8.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/markdown-extra.git", - "reference": "b6e4954ab60030233df5d293886b5404558daac8" + "reference": "e4bf2419df819dcf9dc7a0b25dd8cd1092cbd86d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/b6e4954ab60030233df5d293886b5404558daac8", - "reference": "b6e4954ab60030233df5d293886b5404558daac8", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/e4bf2419df819dcf9dc7a0b25dd8cd1092cbd86d", + "reference": "e4bf2419df819dcf9dc7a0b25dd8cd1092cbd86d", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "twig/twig": "^3.0" }, "require-dev": { @@ -10440,6 +10492,9 @@ }, "type": "library", "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Twig\\Extra\\Markdown\\": "" }, @@ -10467,7 +10522,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/markdown-extra/tree/v3.8.0" + "source": "https://github.com/twigphp/markdown-extra/tree/v3.10.0" }, "funding": [ { @@ -10479,25 +10534,25 @@ "type": "tidelift" } ], - "time": "2023-11-21T14:02:01+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/string-extra", - "version": "v3.8.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", - "reference": "b0c9037d96baff79abe368dc092a59b726517548" + "reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/string-extra/zipball/b0c9037d96baff79abe368dc092a59b726517548", - "reference": "b0c9037d96baff79abe368dc092a59b726517548", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/cd76ed8ae081bcd4fddf549e92e20c5df76c358a", + "reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/string": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^3.0" }, @@ -10534,7 +10589,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.8.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.10.0" }, "funding": [ { @@ -10546,34 +10601,41 @@ "type": "tidelift" } ], - "time": "2023-11-21T14:02:01+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/twig", - "version": "v3.8.0", + "version": "v3.10.3", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" + "reference": "67f29781ffafa520b0bbfbd8384674b42db04572" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572", + "reference": "67f29781ffafa520b0bbfbd8384674b42db04572", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-php80": "^1.22" }, "require-dev": { "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -10606,7 +10668,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.8.0" + "source": "https://github.com/twigphp/Twig/tree/v3.10.3" }, "funding": [ { @@ -10618,7 +10680,7 @@ "type": "tidelift" } ], - "time": "2023-11-21T18:54:41+00:00" + "time": "2024-05-16T10:04:27+00:00" }, { "name": "webmozart/assert", @@ -10779,16 +10841,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.8.3", + "version": "4.10.0", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "598958d8a83cfbd44ba36388b2f9ed69e8b86ed4" + "reference": "2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/598958d8a83cfbd44ba36388b2f9ed69e8b86ed4", - "reference": "598958d8a83cfbd44ba36388b2f9ed69e8b86ed4", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56", + "reference": "2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56", "shasum": "" }, "require": { @@ -10802,7 +10864,7 @@ "require-dev": { "composer/package-versions-deprecated": "^1.11", "doctrine/annotations": "^1.7 || ^2.0", - "friendsofphp/php-cs-fixer": "^2.17 || ^3.0", + "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1", "phpstan/phpstan": "^1.6", "phpunit/phpunit": ">=8", "vimeo/psalm": "^4.23" @@ -10854,31 +10916,30 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.8.3" + "source": "https://github.com/zircote/swagger-php/tree/4.10.0" }, - "time": "2024-01-07T22:33:09+00:00" + "time": "2024-06-06T22:42:02+00:00" } ], "packages-dev": [ { "name": "dama/doctrine-test-bundle", - "version": "v8.0.1", + "version": "v8.2.0", "source": { "type": "git", "url": "https://github.com/dmaicher/doctrine-test-bundle.git", - "reference": "e382d27bc03ee04e0fd0ef95391047042792e7cc" + "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/e382d27bc03ee04e0fd0ef95391047042792e7cc", - "reference": "e382d27bc03ee04e0fd0ef95391047042792e7cc", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", + "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", "shasum": "" }, "require": { "doctrine/dbal": "^3.3 || ^4.0", - "doctrine/doctrine-bundle": "^2.2.2", - "ext-json": "*", - "php": "^7.3 || ^8.0", + "doctrine/doctrine-bundle": "^2.11.0", + "php": "^7.4 || ^8.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "symfony/cache": "^5.4 || ^6.3 || ^7.0", "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0" @@ -10887,7 +10948,7 @@ "behat/behat": "^3.0", "friendsofphp/php-cs-fixer": "^3.27", "phpstan/phpstan": "^1.2", - "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0", "symfony/phpunit-bridge": "^6.3", "symfony/process": "^5.4 || ^6.3 || ^7.0", "symfony/yaml": "^5.4 || ^6.3 || ^7.0" @@ -10924,22 +10985,22 @@ ], "support": { "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", - "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.0.1" + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.2.0" }, - "time": "2023-12-05T16:11:29+00:00" + "time": "2024-05-28T15:41:06+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -10947,11 +11008,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -10977,7 +11039,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -10985,29 +11047,31 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -11015,7 +11079,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -11039,26 +11103,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -11099,9 +11164,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -11156,16 +11227,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.57", + "version": "1.11.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e" + "reference": "490f0ae1c92b082f154681d7849aee776a7c1443" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1627b1d03446904aaa77593f370c5201d2ecc34e", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/490f0ae1c92b082f154681d7849aee776a7c1443", + "reference": "490f0ae1c92b082f154681d7849aee776a7c1443", "shasum": "" }, "require": { @@ -11208,31 +11279,27 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-01-24T11:51:34+00:00" + "time": "2024-06-17T15:10:54+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.59", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "9534fcd0b6906c62594146b506acadeabd3a99b3" + "reference": "dd27a3e83777ba0d9e9cedfaf4ebf95ff67b271f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/9534fcd0b6906c62594146b506acadeabd3a99b3", - "reference": "9534fcd0b6906c62594146b506acadeabd3a99b3", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/dd27a3e83777ba0d9e9cedfaf4ebf95ff67b271f", + "reference": "dd27a3e83777ba0d9e9cedfaf4ebf95ff67b271f", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.48" + "phpstan/phpstan": "^1.11" }, "conflict": { "doctrine/collections": "<1.0", @@ -11242,24 +11309,26 @@ "doctrine/persistence": "<1.3" }, "require-dev": { + "cache/array-adapter": "^1.1", "composer/semver": "^3.3.2", - "doctrine/annotations": "^1.11.0", - "doctrine/collections": "^1.6", + "cweagans/composer-patches": "^1.7.3", + "doctrine/annotations": "^1.11 || ^2.0", + "doctrine/collections": "^1.6 || ^2.1", "doctrine/common": "^2.7 || ^3.0", "doctrine/dbal": "^2.13.8 || ^3.3.3", - "doctrine/lexer": "^1.2.1", - "doctrine/mongodb-odm": "^1.3 || ^2.1", - "doctrine/orm": "^2.14.0", - "doctrine/persistence": "^1.3.8 || ^2.2.1", + "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/mongodb-odm": "^1.3 || ^2.4.3", + "doctrine/orm": "^2.16.0", + "doctrine/persistence": "^2.2.1 || ^3.2", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-phpunit": "^1.3.13", "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.5.10", - "ramsey/uuid-doctrine": "^1.5.0", - "symfony/cache": "^4.4.35" + "phpunit/phpunit": "^9.6.16", + "ramsey/uuid": "^4.2", + "symfony/cache": "^5.4" }, "type": "phpstan-extension", "extra": { @@ -11282,22 +11351,22 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.59" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.4.3" }, - "time": "2024-01-18T09:41:35+00:00" + "time": "2024-06-08T05:48:50+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -11354,7 +11423,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -11362,7 +11431,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -11607,16 +11676,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.16", + "version": "9.6.19", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f" + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", "shasum": "" }, "require": { @@ -11690,7 +11759,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" }, "funding": [ { @@ -11706,20 +11775,20 @@ "type": "tidelift" } ], - "time": "2024-01-19T07:03:14+00:00" + "time": "2024-04-05T04:35:58+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -11754,7 +11823,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -11762,7 +11831,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -12008,16 +12077,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -12062,7 +12131,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -12070,7 +12139,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -12137,16 +12206,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -12202,7 +12271,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -12210,20 +12279,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -12266,7 +12335,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -12274,7 +12343,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -12510,16 +12579,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -12531,7 +12600,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -12552,8 +12621,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -12561,7 +12629,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -12674,16 +12742,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.8.1", + "version": "3.10.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877", + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877", "shasum": "" }, "require": { @@ -12750,20 +12818,20 @@ "type": "open_collective" } ], - "time": "2024-01-11T20:47:48+00:00" + "time": "2024-05-22T21:24:41+00:00" }, { "name": "symfony/debug-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", - "reference": "425c7760a4e6fdc6cb643c791d32277037c971df" + "reference": "689f1bcb0bd3b945e3c671cbd06274b127c64dc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/425c7760a4e6fdc6cb643c791d32277037c971df", - "reference": "425c7760a4e6fdc6cb643c791d32277037c971df", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/689f1bcb0bd3b945e3c671cbd06274b127c64dc9", + "reference": "689f1bcb0bd3b945e3c671cbd06274b127c64dc9", "shasum": "" }, "require": { @@ -12808,7 +12876,7 @@ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/debug-bundle/tree/v6.4.8" }, "funding": [ { @@ -12824,49 +12892,49 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/maker-bundle", - "version": "v1.52.0", + "version": "v1.60.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "112f9466c94a46ca33dc441eee59a12cd1790757" + "reference": "c305a02a22974670f359d4274c9431e1a191f559" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/112f9466c94a46ca33dc441eee59a12cd1790757", - "reference": "112f9466c94a46ca33dc441eee59a12cd1790757", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/c305a02a22974670f359d4274c9431e1a191f559", + "reference": "c305a02a22974670f359d4274c9431e1a191f559", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", - "nikic/php-parser": "^4.11", + "nikic/php-parser": "^4.18|^5.0", "php": ">=8.1", - "symfony/config": "^6.3|^7.0", - "symfony/console": "^6.3|^7.0", - "symfony/dependency-injection": "^6.3|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.3|^7.0", - "symfony/finder": "^6.3|^7.0", - "symfony/framework-bundle": "^6.3|^7.0", - "symfony/http-kernel": "^6.3|^7.0", - "symfony/process": "^6.3|^7.0" + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" }, "conflict": { - "doctrine/doctrine-bundle": "<2.4", - "doctrine/orm": "<2.10" + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" }, "require-dev": { "composer/semver": "^3.0", "doctrine/doctrine-bundle": "^2.5.0", - "doctrine/orm": "^2.10.0", - "symfony/http-client": "^6.3|^7.0", - "symfony/phpunit-bridge": "^6.3|^7.0", - "symfony/security-core": "^6.3|^7.0", - "symfony/yaml": "^6.3|^7.0", - "twig/twig": "^2.0|^3.0" + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" }, "type": "symfony-bundle", "extra": { @@ -12900,7 +12968,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.52.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.60.0" }, "funding": [ { @@ -12916,20 +12984,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T18:23:49+00:00" + "time": "2024-06-10T06:03:18+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "d49b4f6dc4690cf2c194311bb498abf0cf4f7485" + "reference": "937f47cc64922f283bb0c474f33415bba0a9fc0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/d49b4f6dc4690cf2c194311bb498abf0cf4f7485", - "reference": "d49b4f6dc4690cf2c194311bb498abf0cf4f7485", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/937f47cc64922f283bb0c474f33415bba0a9fc0d", + "reference": "937f47cc64922f283bb0c474f33415bba0a9fc0d", "shasum": "" }, "require": { @@ -12961,7 +13029,8 @@ "Symfony\\Bridge\\PhpUnit\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -12981,7 +13050,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.4.8" }, "funding": [ { @@ -12997,20 +13066,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-06-02T15:48:50+00:00" }, { "name": "symfony/process", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "31642b0818bfcff85930344ef93193f8c607e0a3" + "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/31642b0818bfcff85930344ef93193f8c607e0a3", - "reference": "31642b0818bfcff85930344ef93193f8c607e0a3", + "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", "shasum": "" }, "require": { @@ -13042,7 +13111,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.3" + "source": "https://github.com/symfony/process/tree/v6.4.8" }, "funding": [ { @@ -13058,20 +13127,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -13100,7 +13169,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -13108,7 +13177,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], diff --git a/webapp/config/autoload.php.in b/webapp/config/autoload.php.in index 5e3601f839e..30aaf65bc6e 100644 --- a/webapp/config/autoload.php.in +++ b/webapp/config/autoload.php.in @@ -12,8 +12,8 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Composer\Autoload\ClassLoader; // Load the static domserver file if we don't have the constants from it yet -if (!defined('LIBVENDORDIR')) { +if (!defined('VENDORDIR')) { require('@domserver_etcdir@/domserver-static.php'); } -return require LIBVENDORDIR.'/autoload.php'; +return require VENDORDIR.'/autoload.php'; diff --git a/webapp/config/bundles.php b/webapp/config/bundles.php index f759fd8010a..2d4ec46fc96 100644 --- a/webapp/config/bundles.php +++ b/webapp/config/bundles.php @@ -1,4 +1,4 @@ - ['all' => true], @@ -17,4 +17,5 @@ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['prod' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], ]; diff --git a/webapp/config/packages/doctrine.yaml b/webapp/config/packages/doctrine.yaml index 5b95b7a30bd..d04de47c523 100644 --- a/webapp/config/packages/doctrine.yaml +++ b/webapp/config/packages/doctrine.yaml @@ -24,11 +24,12 @@ doctrine: #server_version: '15' orm: auto_generate_proxy_classes: true - enable_lazy_ghost_objects: false report_fields_where_declared: true validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true + controller_resolver: + auto_mapping: false mappings: App: type: attribute diff --git a/webapp/config/packages/framework.yaml b/webapp/config/packages/framework.yaml index ed7df0ab36e..21a3cec7bc7 100644 --- a/webapp/config/packages/framework.yaml +++ b/webapp/config/packages/framework.yaml @@ -4,7 +4,7 @@ framework: esi: false fragments: false http_method_override: true - annotations: true + annotations: false handle_all_throwables: true serializer: enabled: true diff --git a/webapp/config/packages/nelmio_api_doc.yaml b/webapp/config/packages/nelmio_api_doc.yaml index 2453372b42a..0038e04abf3 100644 --- a/webapp/config/packages/nelmio_api_doc.yaml +++ b/webapp/config/packages/nelmio_api_doc.yaml @@ -1,9 +1,11 @@ nelmio_api_doc: + html_config: + assets_mode: bundle documentation: servers: - - url: "%domjudge.baseurl%api" + - url: ~ # Will be set by App\NelmioApiDocBundle\ExternalDocDescriber description: API used at this contest - - url: https://www.domjudge.org/demoweb/api + - url: https://www.domjudge.org/demoweb description: New API in development info: title: DOMjudge @@ -32,15 +34,12 @@ nelmio_api_doc: schema: type: string examples: - int0: - value: "2" - summary: The Demo contest (datasource=0) - int02: - value: "1" - summary: The Demo practice contest (datasource=0) - string: + demo: value: "demo" - summary: The Demo contest (datasource=1) + summary: The Demo contest + demoprac: + value: "demoprac" + summary: The Demo practice contest balloonId: name: balloonId in: path diff --git a/webapp/config/packages/nelmio_cors.yaml b/webapp/config/packages/nelmio_cors.yaml new file mode 100644 index 00000000000..0e5ea4bee8b --- /dev/null +++ b/webapp/config/packages/nelmio_cors.yaml @@ -0,0 +1,7 @@ +nelmio_cors: + paths: + '^/api': + allow_origin: [ '*' ] + allow_credentials: true + allow_methods: [ 'POST', 'PUT', 'GET', 'DELETE' ] + max_age: 3600 diff --git a/webapp/config/packages/security.yaml b/webapp/config/packages/security.yaml index 90be4c137e6..33361a36db3 100644 --- a/webapp/config/packages/security.yaml +++ b/webapp/config/packages/security.yaml @@ -33,8 +33,8 @@ security: # API does Basic Auth and IP address auth api: pattern: ^/api + context: domjudge provider: domjudge_db_provider - stateless: true user_checker: App\Security\UserChecker entry_point: App\Security\DOMJudgeIPAuthenticator # SEE NOTE ABOVE IF CHANGING ANYTHING HERE @@ -45,6 +45,7 @@ security: # Provides prometheus metrics metrics: pattern: ^/prometheus/metrics + context: domjudge provider: domjudge_db_provider stateless: true user_checker: App\Security\UserChecker @@ -57,6 +58,7 @@ security: # rest of app does form_login main: pattern: ^/ + context: domjudge provider: domjudge_db_provider user_checker: App\Security\UserChecker entry_point: App\Security\DOMJudgeXHeadersAuthenticator diff --git a/webapp/config/static.yaml.in b/webapp/config/static.yaml.in index 26983977e68..0a5057aa375 100644 --- a/webapp/config/static.yaml.in +++ b/webapp/config/static.yaml.in @@ -7,7 +7,7 @@ parameters: domjudge.webappdir: @domserver_webappdir@ domjudge.libdir: @domserver_libdir@ domjudge.sqldir: @domserver_sqldir@ - domjudge.libvendordir: @domserver_libvendordir@ + domjudge.vendordir: @domserver_webappdir@/vendor domjudge.logdir: @domserver_logdir@ domjudge.rundir: @domserver_rundir@ domjudge.tmpdir: @domserver_tmpdir@ diff --git a/webapp/migrations/Version20210407120356.php b/webapp/migrations/Version20210407120356.php index 4e7ecffcfad..14c263fae31 100644 --- a/webapp/migrations/Version20210407120356.php +++ b/webapp/migrations/Version20210407120356.php @@ -42,7 +42,6 @@ public function up(Schema $schema) : void for ($idx = 0; $idx < $zip->numFiles; $idx++) { $filename = basename($zip->getNameIndex($idx)); $content = $zip->getFromIndex($idx); - $encodedContent = ($content === '' ? '' : ('0x' . strtoupper(bin2hex($content)))); // In doubt make files executable, but try to read it from the zip file. $executableBit = '1'; @@ -52,21 +51,19 @@ public function up(Schema $schema) : void $executableBit = '0'; } $this->connection->executeStatement( - 'INSERT INTO executable_file ' - . '(`immutable_execid`, `filename`, `ranknumber`, `file_content`, `is_executable`) ' - . 'VALUES (' . $immutable_execid . ', "' . $filename . '", ' - . $idx . ', ' . $encodedContent . ', ' - . $executableBit . ')' + 'INSERT INTO executable_file (`immutable_execid`, `filename`, `ranknumber`, `file_content`, `hash`, `is_executable`)' + . ' VALUES (?, ?, ?, ?, ?, ?)', + [$immutable_execid, $filename, $idx, $content, md5($content), $executableBit] ); } $this->connection->executeStatement( - 'UPDATE executable SET immutable_execid = ' - . $immutable_execid . ' WHERE execid = "' . $oldRow['execid'] . '"' + 'UPDATE executable SET immutable_execid = :immutable_execid WHERE execid = :execid', + ['immutable_execid' => $immutable_execid, 'execid' => $oldRow['execid']] ); } - $this->addSql('ALTER TABLE `executable` DROP COLUMN `zipfile`'); + $this->connection->executeStatement('ALTER TABLE `executable` DROP COLUMN `zipfile`'); } } diff --git a/webapp/migrations/Version20230508120033.php b/webapp/migrations/Version20230508120033.php index b3fe9b2e353..d4d7754433b 100644 --- a/webapp/migrations/Version20230508120033.php +++ b/webapp/migrations/Version20230508120033.php @@ -19,6 +19,9 @@ public function getDescription(): string public function up(Schema $schema): void { + // Cleanup judgetasks for removed submissions + // This can happen when a full contest has been removed and the full delete cascade has failed. + $this->addSql('DELETE from judgetask WHERE submitid not in (SELECT submitid FROM submission)'); // this up() migration is auto-generated, please modify it to your needs $this->addSql('ALTER TABLE judgetask ADD CONSTRAINT FK_83142B703605A691 FOREIGN KEY (submitid) REFERENCES submission (submitid) ON DELETE CASCADE'); } diff --git a/webapp/migrations/Version20230508163415.php b/webapp/migrations/Version20230508163415.php index 9cef0b66baa..0e597425338 100644 --- a/webapp/migrations/Version20230508163415.php +++ b/webapp/migrations/Version20230508163415.php @@ -4,48 +4,28 @@ namespace DoctrineMigrations; -use App\Entity\ExecutableFile; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20230508163415 extends AbstractMigration { + // The code that was in this file was moved to Version20230508180000.php so + // that it will re-run after Version20230508170000.php. This file has been + // kept as a no-op to prevent warnings about previously executed migrations + // that are not registered. + public function getDescription(): string { - return 'Update hashes of immutable executables'; + return '[Deleted]'; } public function up(Schema $schema): void { - $immutableExecutables = $this->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'], - ]); - } + $this->addSql('-- no-op'); // suppress warning "Migration was executed but did not result in any SQL statements." } public function down(Schema $schema): void { - // We don't handle this case - } - - public function isTransactional(): bool - { - return false; + $this->addSql('-- no-op'); } } diff --git a/webapp/migrations/Version20230508170000.php b/webapp/migrations/Version20230508170000.php new file mode 100644 index 00000000000..beb89559413 --- /dev/null +++ b/webapp/migrations/Version20230508170000.php @@ -0,0 +1,41 @@ +connection->fetchAllAssociative('SELECT execfileid, file_content FROM executable_file WHERE hash IS NULL'); + foreach ($executableFiles as $file) { + $this->addSql('UPDATE executable_file SET hash = :hash WHERE execfileid = :id', [ + 'hash' => md5($file['file_content']), + 'id' => $file['execfileid'] + ]); + } + } + + public function down(Schema $schema): void + { + // We don't handle this case + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20230508180000.php b/webapp/migrations/Version20230508180000.php new file mode 100644 index 00000000000..0d11330f79c --- /dev/null +++ b/webapp/migrations/Version20230508180000.php @@ -0,0 +1,50 @@ +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->addSql('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/migrations/Version20231120225210.php b/webapp/migrations/Version20231120225210.php index 4f2bedd3cca..7a4d901b0b4 100644 --- a/webapp/migrations/Version20231120225210.php +++ b/webapp/migrations/Version20231120225210.php @@ -34,7 +34,7 @@ public function up(Schema $schema): void if ($updated) { $newExtensionsJson = json_encode($extensions); - $this->connection->executeQuery('UPDATE language SET extensions = :extensions WHERE langid = :langid', [ + $this->addSql('UPDATE language SET extensions = :extensions WHERE langid = :langid', [ 'extensions' => $newExtensionsJson, 'langid' => $language['langid'], ]); diff --git a/webapp/migrations/Version20240322100827.php b/webapp/migrations/Version20240322100827.php new file mode 100644 index 00000000000..391d414dcda --- /dev/null +++ b/webapp/migrations/Version20240322100827.php @@ -0,0 +1,40 @@ +addSql('CREATE TABLE contest_problemset_content (cid INT UNSIGNED NOT NULL COMMENT \'Contest ID\', content LONGBLOB NOT NULL COMMENT \'Problemset document content(DC2Type:blobtext)\', PRIMARY KEY(cid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Stores contents of contest problemset documents\' '); + $this->addSql('ALTER TABLE contest_problemset_content ADD CONSTRAINT FK_6680FE6A4B30D9C4 FOREIGN KEY (cid) REFERENCES contest (cid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE contest ADD contest_problemset_type VARCHAR(4) DEFAULT NULL COMMENT \'File type of contest problemset document\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE contest_problemset_content DROP FOREIGN KEY FK_6680FE6A4B30D9C4'); + $this->addSql('DROP TABLE contest_problemset_content'); + $this->addSql('ALTER TABLE contest DROP contest_problemset_type'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240322141105.php b/webapp/migrations/Version20240322141105.php new file mode 100644 index 00000000000..94bedbb0e04 --- /dev/null +++ b/webapp/migrations/Version20240322141105.php @@ -0,0 +1,57 @@ +addSql('DELETE b FROM balloon as b LEFT JOIN (SELECT min(b.balloonid) AS min_balloonid FROM balloon as b LEFT JOIN submission as s USING (submitid) GROUP BY teamid, probid, cid) as c ON(b.balloonid = c.min_balloonid) WHERE c.min_balloonid IS NULL'); + + $this->addSql('ALTER TABLE balloon ADD teamid INT UNSIGNED DEFAULT NULL COMMENT \'Team ID\', ADD probid INT UNSIGNED DEFAULT NULL COMMENT \'Problem ID\', ADD cid INT UNSIGNED DEFAULT NULL COMMENT \'Contest ID\''); + $this->addSql('ALTER TABLE balloon ADD CONSTRAINT FK_643B3B904DD6ABF3 FOREIGN KEY (teamid) REFERENCES team (teamid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE balloon ADD CONSTRAINT FK_643B3B90EF049279 FOREIGN KEY (probid) REFERENCES problem (probid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE balloon ADD CONSTRAINT FK_643B3B904B30D9C4 FOREIGN KEY (cid) REFERENCES contest (cid) ON DELETE CASCADE'); + $this->addSql('CREATE INDEX IDX_643B3B904DD6ABF3 ON balloon (teamid)'); + $this->addSql('CREATE INDEX IDX_643B3B90EF049279 ON balloon (probid)'); + $this->addSql('CREATE INDEX IDX_643B3B904B30D9C4 ON balloon (cid)'); + $this->addSql('CREATE UNIQUE INDEX unique_problem ON balloon (cid, teamid, probid)'); + + // copy data + $this->addSql('UPDATE balloon AS b JOIN submission AS s USING (submitid) SET b.teamid = s.teamid, b.probid = s.probid, b.cid = s.cid'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE balloon DROP FOREIGN KEY FK_643B3B904DD6ABF3'); + $this->addSql('ALTER TABLE balloon DROP FOREIGN KEY FK_643B3B90EF049279'); + $this->addSql('ALTER TABLE balloon DROP FOREIGN KEY FK_643B3B904B30D9C4'); + $this->addSql('DROP INDEX IDX_643B3B904DD6ABF3 ON balloon'); + $this->addSql('DROP INDEX IDX_643B3B90EF049279 ON balloon'); + $this->addSql('DROP INDEX IDX_643B3B904B30D9C4 ON balloon'); + $this->addSql('DROP INDEX unique_problem ON balloon'); + $this->addSql('ALTER TABLE balloon DROP teamid, DROP probid, DROP cid'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240323101404.php b/webapp/migrations/Version20240323101404.php new file mode 100644 index 00000000000..6ab8c4f946a --- /dev/null +++ b/webapp/migrations/Version20240323101404.php @@ -0,0 +1,44 @@ +addSql('CREATE TABLE problem_statement_content (probid INT UNSIGNED NOT NULL COMMENT \'Problem ID\', content LONGBLOB NOT NULL COMMENT \'Statement content(DC2Type:blobtext)\', PRIMARY KEY(probid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Stores contents of problem statement\' '); + $this->addSql('ALTER TABLE problem_statement_content ADD CONSTRAINT FK_8A666422EF049279 FOREIGN KEY (probid) REFERENCES problem (probid) ON DELETE CASCADE'); + $this->addSql('INSERT INTO problem_statement_content (probid, content) SELECT probid, content FROM problem_text_content'); + $this->addSql('DROP TABLE problem_text_content'); + $this->addSql('ALTER TABLE problem CHANGE problemtext_type problemstatement_type VARCHAR(4) DEFAULT NULL COMMENT \'File type of problem statement\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE problem_text_content (probid INT UNSIGNED NOT NULL COMMENT \'Problem ID\', content LONGBLOB NOT NULL COMMENT \'Text content(DC2Type:blobtext)\', PRIMARY KEY(probid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Stores contents of problem texts\' '); + $this->addSql('ALTER TABLE problem_text_content ADD CONSTRAINT FK_21B6AD6BEF049279 FOREIGN KEY (probid) REFERENCES problem (probid) ON DELETE CASCADE'); + $this->addSql('INSERT INTO problem_text_content (probid, content) SELECT probid, content FROM problem_statement_content'); + $this->addSql('DROP TABLE problem_statement_content'); + $this->addSql('ALTER TABLE problem CHANGE problemstatement_type problemtext_type VARCHAR(4) DEFAULT NULL COMMENT \'File type of problem text\''); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240511091916.php b/webapp/migrations/Version20240511091916.php new file mode 100644 index 00000000000..2418526d171 --- /dev/null +++ b/webapp/migrations/Version20240511091916.php @@ -0,0 +1,94 @@ + 'gnatmake --version', + 'awk'=> 'awk --version', + 'bash'=> 'bash --version', + 'c' => 'gcc --version', + 'cpp' => 'g++ --version', + 'csharp' => 'mcs --version', + 'f95' => 'gfortran --version', + 'hs' => 'ghc --version', + 'java' => 'javac --version', + 'js' => 'nodejs --version', + 'kt' => 'kotlinc --version', + 'lua' => 'luac -v', + 'pas' => 'fpc -iW', + 'pl' => 'perl -v', + 'plg' => 'swipl --version', + 'py3' => 'pypy3 --version', + 'ocaml' => 'ocamlopt --version', + 'r' => 'Rscript --version', + 'rb' => 'ruby --version', + 'rs' => 'rustc --version', + 'scala' => 'scalac --version', + 'sh' => 'md5sum /bin/sh', + 'swift' => 'swiftc --version']; + + private const RUNNER_VERSION_COMMAND = ['awk'=> 'awk --version', + 'bash'=> 'bash --version', + 'csharp' => 'mono --version', + 'java' => 'java --version', + 'js' => 'nodejs --version', + 'kt' => 'kotlin --version', + 'lua' => 'lua -v', + 'pl' => 'perl -v', + 'py3' => 'pypy3 --version', + 'r' => 'Rscript --version', + 'rb' => 'ruby --version', + 'scala' => 'scala --version', + 'sh' => 'md5sum /bin/sh']; + + public function getDescription(): string + { + return 'Fill default version command for compiler/runner.'; + } + + public function up(Schema $schema): void + { + foreach (self::COMPILER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET compiler_version_command = :compiler_version_command WHERE langid = :langid AND compiler_version_command IS NULL", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + foreach (self::RUNNER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET runner_version_command = :compiler_version_command WHERE langid = :langid AND runner_version_command IS NULL", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + } + + public function down(Schema $schema): void + { + foreach (self::COMPILER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET compiler_version_command = NULL WHERE langid = :langid AND compiler_version_command = :compiler_version_command", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + foreach (self::RUNNER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET runner_version_command = NULL WHERE langid = :langid AND runner_version_command = :compiler_version_command", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240601180624.php b/webapp/migrations/Version20240601180624.php new file mode 100644 index 00000000000..446247dcf17 --- /dev/null +++ b/webapp/migrations/Version20240601180624.php @@ -0,0 +1,38 @@ +addSql('INSERT INTO configuration (name, value) SELECT \'shadow_mode\', 0 FROM configuration WHERE name = \'data_source\' AND value != \'2\''); + $this->addSql('INSERT INTO configuration (name, value) SELECT \'shadow_mode\', 1 FROM configuration WHERE name = \'data_source\' AND value = \'2\''); + $this->addSql('DELETE FROM configuration WHERE name = \'data_source\''); + } + + public function down(Schema $schema): void + { + $this->addSql('INSERT INTO configuration (name, value) SELECT \'data_source\', 1 FROM configuration WHERE name = \'shadow_mode\' AND value = 0'); + $this->addSql('INSERT INTO configuration (name, value) SELECT \'data_source\', 2 FROM configuration WHERE name = \'shadow_mode\' AND value = 1'); + $this->addSql('DELETE FROM configuration WHERE name = \'shadow_mode\''); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240604202419.php b/webapp/migrations/Version20240604202419.php new file mode 100644 index 00000000000..aaed5210512 --- /dev/null +++ b/webapp/migrations/Version20240604202419.php @@ -0,0 +1,70 @@ + 'cid', + 'language' => 'langid', + 'problem' => 'probid', + 'team' => 'teamid', + 'team_affiliation' => 'affilid', + 'team_category' => 'categoryid', + ]; + $notPrefixed = [ + 'clarification' => 'clarid', + 'submission' => 'submitid', + 'user' => 'username', + ]; + + foreach ($djPrefixed as $table => $column) { + $this->setExternalIds($table, $column, 'dj-'); + } + + foreach ($notPrefixed as $table => $column) { + $this->setExternalIds($table, $column); + } + } + + protected function setExternalIds(string $table, string $column, string $prefix = '') + { + $entries = $this->connection->fetchAllAssociative("SELECT $column FROM $table WHERE externalid IS NULL"); + foreach ($entries as $entry) { + $newExternalId = $prefix . $entry[$column]; + // Check if any entity already has this external ID + $existingEntity = $this->connection->fetchAssociative("SELECT externalid FROM $table WHERE externalid = :externalid", ['externalid' => $newExternalId]); + $humanReadableTable = ucfirst(str_replace('_', ' ', $table)); + $this->abortIf((bool)$existingEntity, "$humanReadableTable entity with external ID $newExternalId already exists, manually set a different external ID"); + $this->addSql("UPDATE $table SET externalid = :externalid WHERE $column = :$column", [ + 'externalid' => $newExternalId, + $column => $entry[$column], + ]); + } + } + + public function down(Schema $schema): void + { + // No down migration needed + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/phpstan.dist.neon b/webapp/phpstan.dist.neon similarity index 65% rename from phpstan.dist.neon rename to webapp/phpstan.dist.neon index 8552bfc348f..52c70905c5a 100644 --- a/phpstan.dist.neon +++ b/webapp/phpstan.dist.neon @@ -1,26 +1,25 @@ parameters: level: 6 paths: - - webapp/src - - webapp/tests + - src + - tests excludePaths: - - webapp/src/Utils/Adminer.php + - src/Utils/Adminer.php ignoreErrors: - message: '#Method .* return type has no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: '#Method .* has parameter .* with no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: '#Property .* type has no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: '#PHPDoc tag @var for variable .* has no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: "#Method .* return type has no value type specified in iterable type array#" - path: webapp/src/DataFixtures/Test + path: src/DataFixtures/Test includes: - - phpstan-baseline.neon - - lib/vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-doctrine/extension.neon diff --git a/webapp/phpunit.xml.dist b/webapp/phpunit.xml.dist index 516d82a59d6..21b63889230 100644 --- a/webapp/phpunit.xml.dist +++ b/webapp/phpunit.xml.dist @@ -17,6 +17,10 @@ + + + + diff --git a/webapp/public/css/bootstrap.min.css b/webapp/public/css/bootstrap.min.css index d380a3793ca..8be6e0f2c43 120000 --- a/webapp/public/css/bootstrap.min.css +++ b/webapp/public/css/bootstrap.min.css @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file +../../vendor/twbs/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file diff --git a/webapp/public/css/bootstrap.min.css.map b/webapp/public/css/bootstrap.min.css.map index cef2026e83a..80d05e88d05 120000 --- a/webapp/public/css/bootstrap.min.css.map +++ b/webapp/public/css/bootstrap.min.css.map @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/css/bootstrap.min.css.map \ No newline at end of file +../../vendor/twbs/bootstrap/dist/css/bootstrap.min.css.map \ No newline at end of file diff --git a/webapp/public/css/dataTables.bootstrap5.min.css b/webapp/public/css/dataTables.bootstrap5.min.css index ed9929d74dc..c2bb4544bfe 120000 --- a/webapp/public/css/dataTables.bootstrap5.min.css +++ b/webapp/public/css/dataTables.bootstrap5.min.css @@ -1 +1 @@ -../../../lib/vendor/datatables.net/datatables.net-bs5/css/dataTables.bootstrap5.min.css \ No newline at end of file +../../vendor/datatables.net/datatables.net-bs5/css/dataTables.bootstrap5.min.css \ No newline at end of file diff --git a/webapp/public/css/fontawesome-all.min.css b/webapp/public/css/fontawesome-all.min.css index d07e91bcd35..a95a7f46b0e 120000 --- a/webapp/public/css/fontawesome-all.min.css +++ b/webapp/public/css/fontawesome-all.min.css @@ -1 +1 @@ -../../../lib/vendor/fortawesome/font-awesome/css/all.min.css \ No newline at end of file +../../vendor/fortawesome/font-awesome/css/all.min.css \ No newline at end of file diff --git a/webapp/public/css/nv.d3.min.css b/webapp/public/css/nv.d3.min.css index 51a52083765..9a26ecc6cda 120000 --- a/webapp/public/css/nv.d3.min.css +++ b/webapp/public/css/nv.d3.min.css @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.css \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.css \ No newline at end of file diff --git a/webapp/public/css/nv.d3.min.css.map b/webapp/public/css/nv.d3.min.css.map index 3a322fb4703..220b5722a55 120000 --- a/webapp/public/css/nv.d3.min.css.map +++ b/webapp/public/css/nv.d3.min.css.map @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.css.map \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.css.map \ No newline at end of file diff --git a/webapp/public/css/select2-bootstrap-5-theme.min.css b/webapp/public/css/select2-bootstrap-5-theme.min.css index 407fb9f5f9a..cd8fd07b88f 120000 --- a/webapp/public/css/select2-bootstrap-5-theme.min.css +++ b/webapp/public/css/select2-bootstrap-5-theme.min.css @@ -1 +1 @@ -../../../lib/vendor/apalfrey/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css \ No newline at end of file +../../vendor/apalfrey/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css \ No newline at end of file diff --git a/webapp/public/css/select2.min.css b/webapp/public/css/select2.min.css index 2eda641b12c..adb8a9e6fcc 120000 --- a/webapp/public/css/select2.min.css +++ b/webapp/public/css/select2.min.css @@ -1 +1 @@ -../../../lib/vendor/select2/select2/dist/css/select2.min.css \ No newline at end of file +../../vendor/select2/select2/dist/css/select2.min.css \ No newline at end of file diff --git a/webapp/public/flags b/webapp/public/flags index 1dde7d80c95..d54be4395b0 120000 --- a/webapp/public/flags +++ b/webapp/public/flags @@ -1 +1 @@ -../../lib/vendor/components/flag-icon-css/flags \ No newline at end of file +../vendor/components/flag-icon-css/flags \ No newline at end of file diff --git a/webapp/public/index.php b/webapp/public/index.php index dccc8f0ae86..1fd07b5fb53 100644 --- a/webapp/public/index.php +++ b/webapp/public/index.php @@ -2,7 +2,7 @@ use App\Kernel; -require_once dirname(__DIR__, 2) . '/lib/vendor/autoload_runtime.php'; +require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; require_once dirname(__DIR__) . '/config/load_db_secrets.php'; return function (array $context) { diff --git a/webapp/public/js/FileSaver.min.js b/webapp/public/js/FileSaver.min.js index db9159a068d..2d4de7b2785 120000 --- a/webapp/public/js/FileSaver.min.js +++ b/webapp/public/js/FileSaver.min.js @@ -1 +1 @@ -../../../lib/vendor/eligrey/filesaver/dist/FileSaver.min.js \ No newline at end of file +../../vendor/eligrey/filesaver/dist/FileSaver.min.js \ No newline at end of file diff --git a/webapp/public/js/FileSaver.min.js.map b/webapp/public/js/FileSaver.min.js.map index 587c5cbaa63..8c92a5a1b10 120000 --- a/webapp/public/js/FileSaver.min.js.map +++ b/webapp/public/js/FileSaver.min.js.map @@ -1 +1 @@ -../../../lib/vendor/eligrey/filesaver/dist/FileSaver.min.js.map \ No newline at end of file +../../vendor/eligrey/filesaver/dist/FileSaver.min.js.map \ No newline at end of file diff --git a/webapp/public/js/bootstrap.bundle.min.js b/webapp/public/js/bootstrap.bundle.min.js index a0994828bc1..0889d279571 120000 --- a/webapp/public/js/bootstrap.bundle.min.js +++ b/webapp/public/js/bootstrap.bundle.min.js @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js \ No newline at end of file +../../vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js \ No newline at end of file diff --git a/webapp/public/js/bootstrap.bundle.min.js.map b/webapp/public/js/bootstrap.bundle.min.js.map index 5f0e462a2e5..61f03fce666 120000 --- a/webapp/public/js/bootstrap.bundle.min.js.map +++ b/webapp/public/js/bootstrap.bundle.min.js.map @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js.map \ No newline at end of file +../../vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/webapp/public/js/d3.min.js b/webapp/public/js/d3.min.js index 9fb1b29cbc9..27b19c4f7ab 120000 --- a/webapp/public/js/d3.min.js +++ b/webapp/public/js/d3.min.js @@ -1 +1 @@ -../../../lib/vendor/mbostock/d3/d3.min.js \ No newline at end of file +../../vendor/mbostock/d3/d3.min.js \ No newline at end of file diff --git a/webapp/public/js/dataTables.bootstrap5.min.js b/webapp/public/js/dataTables.bootstrap5.min.js index 9f856d06ea9..3b9c9206a6d 120000 --- a/webapp/public/js/dataTables.bootstrap5.min.js +++ b/webapp/public/js/dataTables.bootstrap5.min.js @@ -1 +1 @@ -../../../lib/vendor/datatables.net/datatables.net-bs5/js/dataTables.bootstrap5.min.js \ No newline at end of file +../../vendor/datatables.net/datatables.net-bs5/js/dataTables.bootstrap5.min.js \ No newline at end of file diff --git a/webapp/public/js/dataTables.min.js b/webapp/public/js/dataTables.min.js index 4f945df306f..179e4bf6256 120000 --- a/webapp/public/js/dataTables.min.js +++ b/webapp/public/js/dataTables.min.js @@ -1 +1 @@ -../../../lib/vendor/datatables.net/datatables.net/js/dataTables.min.js \ No newline at end of file +../../vendor/datatables.net/datatables.net/js/dataTables.min.js \ No newline at end of file diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index 1cba40b466d..0e9dde24efb 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -55,6 +55,22 @@ function disableNotifications() return true; } +function enableKeys() +{ + setCookie('domjudge_keys', 1); + $("#keys_disable").removeClass('d-none'); + $("#keys_disable").show(); + $("#keys_enable").hide(); +} + +function disableKeys() +{ + setCookie('domjudge_keys', 0); + $("#keys_enable").removeClass('d-none'); + $("#keys_enable").show(); + $("#keys_disable").hide(); +} + // Send a notification if notifications have been enabled. // The options argument is passed to the Notification constructor, // except that the following tags (if found) are interpreted and @@ -117,21 +133,6 @@ function collapse(x) $(x).toggleClass('d-none'); } -function togglelastruns() -{ - var names = {'lastruntime':0, 'lastresult':1, 'lasttcruns':2}; - for (var name in names) { - var cells = document.getElementsByClassName(name); - for (var i = 0; i < cells.length; i++) { - var style = 'inline'; - if (name === 'lasttcruns') { - style = 'table-row'; - } - cells[i].style.display = (cells[i].style.display === 'none') ? style : 'none'; - } - } -} - // TODO: We should probably reload the page if the clock hits contest // start (and end?). function updateClock() @@ -822,5 +823,118 @@ function setupPreviewClarification($input, $previewDiv, previewInitial) { } $(function () { - $('[data-toggle="tooltip"]').tooltip(); + $('[data-bs-toggle="tooltip"]').tooltip(); }); + +function initializeKeyboardShortcuts() { + var $body = $('body'); + var ignore = false; + $body.on('keydown', function(e) { + var keysCookie = getCookie('domjudge_keys'); + if (keysCookie != 1 && keysCookie != "") { + return; + } + // Check if the user is not typing in an input field. + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + return; + } + var key = e.key.toLowerCase(); + if (key === '?') { + var $keyhelp = $('#keyhelp'); + if ($keyhelp.length) { + $keyhelp.toggleClass('d-none'); + } + return; + } + if (key === 'escape') { + var $keyhelp = $('#keyhelp'); + if ($keyhelp.length && !$keyhelp.hasClass('d-none')) { + $keyhelp.addClass('d-none'); + } + } + + if (!ignore && !e.shiftKey && (key === 'j' || key === 'k')) { + var parts = window.location.href.split('/'); + var lastPart = parts[parts.length - 1]; + var params = lastPart.split('?'); + var currentNumber = parseInt(params[0]); + if (isNaN(currentNumber)) { + return; + } + if (key === 'j') { + parts[parts.length - 1] = currentNumber + 1; + } else if (key === 'k') { + parts[parts.length - 1] = currentNumber - 1; + } + if (params.length > 1) { + parts[parts.length - 1] += '?' + params[1]; + } + window.location = parts.join('/'); + } else if (!ignore && (key === 's' || key === 't' || key === 'p' || key === 'j' || key === 'c')) { + if (e.shiftKey && key === 's') { + window.location = domjudge_base_url + '/jury/scoreboard'; + return; + } + var type = key; + ignore = true; + var oldFunc = null; + var events = $._data($body[0], 'events'); + if (events && events.keydown) { + oldFunc = events.keydown[0].handler; + } + var sequence = ''; + var box = null; + var $sequenceBox = $('
'); + box = $sequenceBox; + $sequenceBox.text(type + sequence); + $sequenceBox.appendTo($body); + $body.on('keydown', function(e) { + // Check if the user is not typing in an input field. + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + ignore = false; + if (box) { + box.remove(); + } + sequence = ''; + return; + } + if (e.key >= '0' && e.key <= '9') { + sequence += e.key; + box.text(type + sequence); + } else if (e.key === 'Enter') { + ignore = false; + switch (type) { + case 's': + type = 'submissions'; + break; + case 't': + type = 'teams'; + break; + case 'p': + type = 'problems'; + break; + case 'c': + type = 'clarifications'; + break; + case 'j': + window.location = domjudge_base_url + '/jury/submissions/by-judging-id/' + sequence; + return; + } + var redirect_to = domjudge_base_url + '/jury/' + type; + if (sequence) { + redirect_to += '/' + sequence; + } + window.location = redirect_to; + } else { + ignore = false; + if (box) { + box.remove(); + } + sequence = ''; + $body.off('keydown'); + $body.on('keydown', oldFunc); + } + }); + } + }); +} diff --git a/webapp/public/js/jquery.min.js b/webapp/public/js/jquery.min.js index 6afb7f5993b..2380e192170 120000 --- a/webapp/public/js/jquery.min.js +++ b/webapp/public/js/jquery.min.js @@ -1 +1 @@ -../../../lib/vendor/components/jquery/jquery.min.js \ No newline at end of file +../../vendor/components/jquery/jquery.min.js \ No newline at end of file diff --git a/webapp/public/js/nv.d3.min.js b/webapp/public/js/nv.d3.min.js index ab7aa7313e9..319ecf7354c 120000 --- a/webapp/public/js/nv.d3.min.js +++ b/webapp/public/js/nv.d3.min.js @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.js \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.js \ No newline at end of file diff --git a/webapp/public/js/nv.d3.min.js.map b/webapp/public/js/nv.d3.min.js.map index 6c57c82185e..b4dd186d125 120000 --- a/webapp/public/js/nv.d3.min.js.map +++ b/webapp/public/js/nv.d3.min.js.map @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.js.map \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.js.map \ No newline at end of file diff --git a/webapp/public/js/select2.min.js b/webapp/public/js/select2.min.js index f71b2857a35..491cfe1ed45 120000 --- a/webapp/public/js/select2.min.js +++ b/webapp/public/js/select2.min.js @@ -1 +1 @@ -../../../lib/vendor/select2/select2/dist/js/select2.min.js \ No newline at end of file +../../vendor/select2/select2/dist/js/select2.min.js \ No newline at end of file diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index b019e8fa91a..f7c69f890ff 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -84,9 +84,9 @@ a { .prevsubmit { color: #696969; } .output_text { - border-top: 1px dotted #C0C0C0; - border-bottom: 1px dotted #C0C0C0; - background-color: #FAFAFA; + border-top: 1px dotted #c0c0c0; + border-bottom: 1px dotted #c0c0c0; + background-color: #fafafa; margin: 0; padding: 5px; font-family: monospace; @@ -94,9 +94,9 @@ a { } .clarificationform pre { - border-top: 1px dotted #C0C0C0; - border-bottom: 1px dotted #C0C0C0; - background-color: #FAFAFA; + border-top: 1px dotted #c0c0c0; + border-bottom: 1px dotted #c0c0c0; + background-color: #fafafa; margin: 0; padding: 5px; font-family: monospace; @@ -104,7 +104,7 @@ a { } kbd { - background-color: #FAFAFA; + background-color: #fafafa; color: black; } @@ -227,7 +227,7 @@ del { right: 0; bottom: 0; } -.scoreboard .scoreaf { white-space: nowrap; border: 0; text-align: center; } +.scoreboard .scoreaf { white-space: nowrap; border: 0; padding-left: 2px; text-align: center; } .scoreboard .scoreaf img { vertical-align: middle; } .univ { font-size: 80%; @@ -265,14 +265,14 @@ img.affiliation-logo { padding-right: 3pt; } -.score_correct { background: #60e760; } -.score_first { background: #1daa1d !important; } -.score_pending { background: #6666FF; } -.score_incorrect { background: #e87272; } +.score_correct { background: #60e760; } +.score_correct.score_first { background: #1daa1d; } +.score_incorrect { background: #e87272; } +.score_pending { background: #6666ff; } -.gold-medal { background-color: #EEC710 } -.silver-medal { background-color: #AAA } -.bronze-medal { background-color: #C08E55 } +.gold-medal { background-color: #eec710 } +.silver-medal { background-color: #aaa } +.bronze-medal { background-color: #c08e55 } #scoresolv,#scoretotal { width: 2.5em; } .scorenc,.scorett,.scorepl { text-align: center; width: 2ex; } @@ -337,10 +337,11 @@ td.scorenc { border-color: silver; border-right: 0; } } .countryflag { - height: 30px; - width: 40px; - border-radius: 8px; - padding: 2px; + height: 1.5rem; + width: 2rem; + border-radius: .375rem; + margin: .125rem; + box-shadow: 0 0 0 1px rgba(0, 0, 0, .2); } .select2 img.countryflag { @@ -400,7 +401,7 @@ tr.ignore td, td.ignore, span.ignore { margin-right: auto; } -#teampicture { +.teampicture { width: 100%; border: 1px solid black; } @@ -618,7 +619,9 @@ tr.ignore td, td.ignore, span.ignore { } .problem-badge { - font-size: 100%; + --badge-padding-x: 0.5em; + font-size: 1em; + min-width: 2em; } .tooltip .tooltip-inner { @@ -646,3 +649,53 @@ blockquote { font-style: italic; font-size: smaller; } + +.right { + text-align: right; +} + +#contesttimer { + color: DimGray; + margin-left: auto; +} + +.lasttcruns, .lastresult { + opacity: 0.5; +} + +.keybox { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #295d8a; + font-size: 200%; + font-weight: bold; + color: white; + padding: 20px; + border-radius: 5px; + z-index: 1000; +} + +#keyhelp { + position: fixed; + height: 90%; + width: 90%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(41, 93, 138, 0.9); + font-size: 150%; + font-weight: bold; + color: white; + padding: 20px; + border-radius: 5px; + z-index: 1001; +} + +#keyhelp code { + color: white; + background-color: black; + padding: 3px; + border-radius: 5px; +} diff --git a/webapp/public/style_jury.css b/webapp/public/style_jury.css index 9c3ad918371..9cc0bcfc211 100644 --- a/webapp/public/style_jury.css +++ b/webapp/public/style_jury.css @@ -48,6 +48,11 @@ tr.summary td { border-top: 1px solid black; } clickable in the jury scoreboard and for all team cells */ .scoreboard_jury td, .scoreboard_jury th { padding: 0; } +/* show pending submissions using a blue corner */ +.score_pending.score_correct { background: linear-gradient(45deg, #60e760 85%, #6666ff 85%); } +.score_pending.score_correct.score_first { background: linear-gradient(45deg, #1daa1d 85%, #6666ff 85%); } +.score_pending.score_incorrect { background: linear-gradient(45deg, #e87272 85%, #6666ff 85%); } + #submission_layout { width: 100%; } #djlogo { @@ -191,7 +196,7 @@ table.submissions-table { } .devmode { - background-color: #295D8A !important; + background-color: #295d8a !important; } .devmode-icon { @@ -239,3 +244,7 @@ table.table-full-clickable-cell tr .table-button-head-right-right{ .execid { font-family: monospace; } + +.timebutton { + min-width: 9em; +} diff --git a/webapp/public/webfonts b/webapp/public/webfonts index 3478e1aff45..a7aa14cc23f 120000 --- a/webapp/public/webfonts +++ b/webapp/public/webfonts @@ -1 +1 @@ -../../lib/vendor/fortawesome/font-awesome/webfonts \ No newline at end of file +../vendor/fortawesome/font-awesome/webfonts \ No newline at end of file diff --git a/webapp/src/Command/AbstractCompareCommand.php b/webapp/src/Command/AbstractCompareCommand.php new file mode 100644 index 00000000000..99c0d2cfc46 --- /dev/null +++ b/webapp/src/Command/AbstractCompareCommand.php @@ -0,0 +1,96 @@ + $compareService + */ + public function __construct( + protected readonly SerializerInterface $serializer, + protected AbstractCompareService $compareService + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('file1', InputArgument::REQUIRED, 'First file to compare') + ->addArgument('file2', InputArgument::REQUIRED, 'Second file to compare'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + $messages = $this->compareService->compareFiles($input->getArgument('file1'), $input->getArgument('file2')); + + return $this->displayMessages($style, $messages) ?? Command::SUCCESS; + } + + /** + * @param Message[] $messages + */ + protected function displayMessages(SymfonyStyle $style, array $messages): ?int + { + if (empty($messages)) { + $style->success('Files match fully'); + return null; + } + + $headers = ['Level', 'Message', 'Source', 'Target']; + $rows = []; + $counts = []; + foreach ($messages as $message) { + if (!isset($counts[$message->type->value])) { + $counts[$message->type->value] = 0; + } + $counts[$message->type->value]++; + $rows[] = [ + $this->formatMessage($message->type, $message->type->value), + $this->formatMessage($message->type, $message->message), + $this->formatMessage($message->type, $message->source ?? ''), + $this->formatMessage($message->type, $message->target ?? ''), + ]; + } + $style->table($headers, $rows); + + $style->newLine(); + foreach ($counts as $type => $count) { + $style->writeln($this->formatMessage(MessageType::from($type), sprintf('Found %d %s(s)', $count, $type))); + } + + if (isset($counts['error'])) { + $style->error('Files have potential critical differences'); + return Command::FAILURE; + } + + $style->success('Files have differences but probably non critical'); + + return null; + } + + protected function formatMessage(MessageType $level, string $message): string + { + $colors = [ + MessageType::ERROR->value => 'red', + MessageType::WARNING->value => 'yellow', + MessageType::INFO->value => 'green', + ]; + return sprintf('%s', $colors[$level->value], $message); + } +} diff --git a/webapp/src/Command/CheckDatabaseConfigurationDefaultValuesCommand.php b/webapp/src/Command/CheckDatabaseConfigurationDefaultValuesCommand.php index ffbe6dc0cff..5fe5e243bca 100644 --- a/webapp/src/Command/CheckDatabaseConfigurationDefaultValuesCommand.php +++ b/webapp/src/Command/CheckDatabaseConfigurationDefaultValuesCommand.php @@ -27,38 +27,38 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($this->config->getConfigSpecification() as $specification) { $message = sprintf( 'Configuration %s (in category %s) is of type %s but has wrong type for default_value (%s)', - $specification['name'], - $specification['category'], - $specification['type'], - json_encode($specification['default_value'], JSON_THROW_ON_ERROR) + $specification->name, + $specification->category, + $specification->type, + json_encode($specification->defaultValue, JSON_THROW_ON_ERROR) ); - switch ($specification['type']) { + switch ($specification->type) { case 'bool': - if (!is_bool($specification['default_value'])) { + if (!is_bool($specification->defaultValue)) { $messages[] = $message; } break; case 'int': - if (!is_int($specification['default_value'])) { + if (!is_int($specification->defaultValue)) { $messages[] = $message; } break; case 'string': - if (!is_string($specification['default_value'])) { + if (!is_string($specification->defaultValue)) { $messages[] = $message; } break; case 'array_val': - if (!(empty($specification['default_value']) || ( - is_array($specification['default_value']) && - is_int(key($specification['default_value']))))) { + if (!(empty($specification->defaultValue) || ( + is_array($specification->defaultValue) && + is_int(key($specification->defaultValue))))) { $messages[] = $message; } break; case 'array_keyval': - if (!(empty($specification['default_value']) || ( - is_array($specification['default_value']) && - is_string(key($specification['default_value']))))) { + if (!(empty($specification->defaultValue) || ( + is_array($specification->defaultValue) && + is_string(key($specification->defaultValue))))) { $messages[] = $message; } break; diff --git a/webapp/src/Command/CompareAwardsCommand.php b/webapp/src/Command/CompareAwardsCommand.php new file mode 100644 index 00000000000..757c73f29cf --- /dev/null +++ b/webapp/src/Command/CompareAwardsCommand.php @@ -0,0 +1,25 @@ + + */ +#[AsCommand( + name: 'compare:awards', + description: 'Compare awards between two files' +)] +class CompareAwardsCommand extends AbstractCompareCommand +{ + public function __construct( + SerializerInterface $serializer, + AwardCompareService $compareService + ) { + parent::__construct($serializer, $compareService); + } +} diff --git a/webapp/src/Command/CompareResultsCommand.php b/webapp/src/Command/CompareResultsCommand.php new file mode 100644 index 00000000000..f12d0ced8e2 --- /dev/null +++ b/webapp/src/Command/CompareResultsCommand.php @@ -0,0 +1,25 @@ + + */ +#[AsCommand( + name: 'compare:results', + description: 'Compare results between two files' +)] +class CompareResultsCommand extends AbstractCompareCommand +{ + public function __construct( + SerializerInterface $serializer, + ResultsCompareService $compareService + ) { + parent::__construct($serializer, $compareService); + } +} diff --git a/webapp/src/Command/CompareScoreboardCommand.php b/webapp/src/Command/CompareScoreboardCommand.php new file mode 100644 index 00000000000..c4eb4b8c031 --- /dev/null +++ b/webapp/src/Command/CompareScoreboardCommand.php @@ -0,0 +1,25 @@ + + */ +#[AsCommand( + name: 'compare:scoreboard', + description: 'Compare scoreboard between two files' +)] +class CompareScoreboardCommand extends AbstractCompareCommand +{ + public function __construct( + SerializerInterface $serializer, + ScoreboardCompareService $compareService + ) { + parent::__construct($serializer, $compareService); + } +} diff --git a/webapp/src/Command/ImportEventFeedCommand.php b/webapp/src/Command/ImportEventFeedCommand.php index 594a6fb6118..4b1960059b5 100644 --- a/webapp/src/Command/ImportEventFeedCommand.php +++ b/webapp/src/Command/ImportEventFeedCommand.php @@ -39,6 +39,7 @@ class ImportEventFeedCommand extends Command public function __construct( protected readonly EntityManagerInterface $em, protected readonly ConfigurationService $config, + protected readonly DOMJudgeService $dj, protected readonly TokenStorageInterface $tokenStorage, protected readonly ?Profiler $profiler, protected readonly ExternalContestSourceService $sourceService, @@ -102,15 +103,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $dataSource = (int)$this->config->get('data_source'); - $importDataSource = DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; - if ($dataSource !== $importDataSource) { - $dataSourceOptions = $this->config->getConfigSpecification()['data_source']['options']; - $this->style->error(sprintf( - "data_source configuration setting is set to '%s' but should be '%s'.", - $dataSourceOptions[$dataSource], - $dataSourceOptions[$importDataSource] - )); + if (!$this->dj->shadowMode()) { + $this->style->error("shadow_mode configuration setting is set to 'false' but should be 'true'."); return Command::FAILURE; } diff --git a/webapp/src/Controller/API/AbstractApiController.php b/webapp/src/Controller/API/AbstractApiController.php new file mode 100644 index 00000000000..f35de779d90 --- /dev/null +++ b/webapp/src/Controller/API/AbstractApiController.php @@ -0,0 +1,94 @@ +em->createQueryBuilder(); + $qb + ->from(Contest::class, 'c') + ->select('c') + ->andWhere('c.enabled = 1') + ->orderBy('c.activatetime'); + + if ($onlyActive || !$this->dj->checkrole('api_reader')) { + $qb + ->andWhere('c.activatetime <= :now') + ->andWhere('c.deactivatetime IS NULL OR c.deactivatetime > :now') + ->setParameter('now', $now); + } + + // Filter on contests this user has access to + if (!$this->dj->checkrole('api_reader') && !$this->dj->checkrole('judgehost')) { + if ($this->dj->checkrole('team') && $this->dj->getUser()->getTeam()) { + $qb->leftJoin('c.teams', 'ct') + ->leftJoin('c.team_categories', 'tc') + ->leftJoin('tc.teams', 'tct') + ->andWhere('ct.teamid = :teamid OR tct.teamid = :teamid OR c.openToAllTeams = 1') + ->setParameter('teamid', $this->dj->getUser()->getTeam()); + } else { + $qb->andWhere('c.public = 1'); + } + } + + return $qb; + } + + /** + * @throws NonUniqueResultException + */ + protected function getContestId(Request $request): int + { + if (!$request->attributes->has('cid')) { + throw new BadRequestHttpException('cid parameter missing'); + } + + $qb = $this->getContestQueryBuilder($request->query->getBoolean('onlyActive', false)); + $qb + ->andWhere('c.externalid = :cid') + ->setParameter('cid', $request->attributes->get('cid')); + + /** @var Contest|null $contest */ + $contest = $qb->getQuery()->getOneOrNullResult(); + + if ($contest === null) { + throw new NotFoundHttpException(sprintf('Contest with ID \'%s\' not found', $request->attributes->get('cid'))); + } + + return $contest->getCid(); + } +} diff --git a/webapp/src/Controller/API/AbstractRestController.php b/webapp/src/Controller/API/AbstractRestController.php index 114aebd1c1c..ba52dd120f9 100644 --- a/webapp/src/Controller/API/AbstractRestController.php +++ b/webapp/src/Controller/API/AbstractRestController.php @@ -2,40 +2,24 @@ namespace App\Controller\API; -use App\Entity\Contest; -use App\Service\ConfigurationService; -use App\Service\DOMJudgeService; -use App\Service\EventLogService; -use App\Utils\Utils; -use Doctrine\ORM\EntityManagerInterface; +use App\Entity\BaseApiEntity; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\QueryBuilder; -use Exception; -use FOS\RestBundle\Controller\AbstractFOSRestController; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; -abstract class AbstractRestController extends AbstractFOSRestController +/** + * @template T of BaseApiEntity + * @template U + */ +abstract class AbstractRestController extends AbstractApiController { - final public const GROUP_DEFAULT = 'Default'; - final public const GROUP_NONSTRICT = 'Nonstrict'; - final public const GROUP_RESTRICTED = 'Restricted'; - final public const GROUP_RESTRICTED_NONSTRICT = 'RestrictedNonstrict'; - - public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, - protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLogService - ) {} - /** * Get all objects for this endpoint. * @throws NonUniqueResultException @@ -55,25 +39,9 @@ protected function performSingleAction(Request $request, string $id): Response // by internal requests. $this->em->clear(); - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID. - $externalIdAlwaysAllowed = [ - 's.submitid', - 'clar.clarid', - ]; - $idField = $this->getIdField(); - if (in_array($idField, $externalIdAlwaysAllowed)) { - $table = explode('.', $idField)[0]; - $queryBuilder = $this->getQueryBuilder($request) - ->andWhere(sprintf('(%s.externalid IS NULL AND %s = :id) OR %s.externalid = :id', $table, $idField, $table)) - ->setParameter('id', $id); - } else { - $queryBuilder = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $idField)) - ->setParameter('id', $id); - } - - $object = $queryBuilder + $object = $this->getQueryBuilder($request) + ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -125,14 +93,12 @@ protected function renderData( /** * Render the given create data using the correct groups. - * - * @param string|int $id */ protected function renderCreateData( Request $request, mixed $data, string $routeType, - $id + int|string $id ): Response { $params = [ 'id' => $id, @@ -153,76 +119,6 @@ protected function renderCreateData( $headers); } - /** - * Get the query builder used for getting contests. - * @param bool $onlyActive return only contests that are active - */ - protected function getContestQueryBuilder(bool $onlyActive = false): QueryBuilder - { - $now = Utils::now(); - $qb = $this->em->createQueryBuilder(); - $qb - ->from(Contest::class, 'c') - ->select('c') - ->andWhere('c.enabled = 1') - ->orderBy('c.activatetime'); - - if ($onlyActive || !$this->dj->checkrole('api_reader')) { - $qb - ->andWhere('c.activatetime <= :now') - ->andWhere('c.deactivatetime IS NULL OR c.deactivatetime > :now') - ->setParameter('now', $now); - } - - // Filter on contests this user has access to - if (!$this->dj->checkrole('api_reader') && !$this->dj->checkrole('judgehost')) { - if ($this->dj->checkrole('team') && $this->dj->getUser()->getTeam()) { - $qb->leftJoin('c.teams', 'ct') - ->leftJoin('c.team_categories', 'tc') - ->leftJoin('tc.teams', 'tct') - ->andWhere('ct.teamid = :teamid OR tct.teamid = :teamid OR c.openToAllTeams = 1') - ->setParameter('teamid', $this->dj->getUser()->getTeam()); - } else { - $qb->andWhere('c.public = 1'); - } - } - - return $qb; - } - - /** - * @throws NonUniqueResultException - */ - protected function getContestId(Request $request): int - { - if (!$request->attributes->has('cid')) { - throw new BadRequestHttpException('cid parameter missing'); - } - - $qb = $this->getContestQueryBuilder($request->query->getBoolean('onlyActive', false)); - $qb - ->andWhere(sprintf('c.%s = :cid', $this->getContestIdField())) - ->setParameter('cid', $request->attributes->get('cid')); - - /** @var Contest|null $contest */ - $contest = $qb->getQuery()->getOneOrNullResult(); - - if ($contest === null) { - throw new NotFoundHttpException(sprintf('Contest with ID \'%s\' not found', $request->attributes->get('cid'))); - } - - return $contest->getCid(); - } - - protected function getContestIdField(): string - { - try { - return $this->eventLogService->externalIdFieldForEntity(Contest::class) ?? 'cid'; - } catch (Exception) { - return 'cid'; - } - } - /** * Get the query builder to use for request for this REST endpoint. * @throws NonUniqueResultException @@ -235,6 +131,7 @@ abstract protected function getQueryBuilder(Request $request): QueryBuilder; abstract protected function getIdField(): string; /** + * @return array * @throws NonUniqueResultException */ protected function listActionHelper(Request $request): array @@ -246,32 +143,13 @@ protected function listActionHelper(Request $request): array if ($request->query->has('ids')) { $ids = $request->query->all('ids'); - $ids = array_unique($ids); - - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID. - $externalIdAlwaysAllowed = [ - 's.submitid', - 'clar.clarid', - ]; - $idField = $this->getIdField(); - if (in_array($idField, $externalIdAlwaysAllowed)) { - $table = explode('.', $idField)[0]; - $or = $queryBuilder->expr()->orX(); - foreach ($ids as $index => $id) { - $or->add(sprintf('(%s.externalid IS NULL AND %s = :id%s) OR %s.externalid = :id%s', $table, $idField, $index, $table, $index)); - $queryBuilder->setParameter(sprintf('id%s', $index), $id); - } - $queryBuilder->andWhere($or); - } else { - $queryBuilder - ->andWhere(sprintf('%s IN (:ids)', $this->getIdField())) - ->setParameter('ids', $ids); - } + $queryBuilder + ->andWhere(sprintf('%s IN (:ids)', $this->getIdField())) + ->setParameter('ids', $ids); } - /** @var array $objects */ + /** @var array $objects */ $objects = $queryBuilder ->getQuery() ->getResult(); diff --git a/webapp/src/Controller/API/AccessController.php b/webapp/src/Controller/API/AccessController.php index 4cd8203b87a..68b8c5d2f2d 100644 --- a/webapp/src/Controller/API/AccessController.php +++ b/webapp/src/Controller/API/AccessController.php @@ -6,8 +6,6 @@ use App\DataTransferObject\AccessEndpoint; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; -use Doctrine\ORM\QueryBuilder; -use Exception; use FOS\RestBundle\Controller\Annotations as Rest; use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; @@ -22,7 +20,7 @@ #[OA\Response(ref: '#/components/responses/Unauthenticated', response: 401)] #[OA\Response(ref: '#/components/responses/Unauthorized', response: 403)] #[OA\Response(ref: '#/components/responses/NotFound', response: 404)] -class AccessController extends AbstractRestController +class AccessController extends AbstractApiController { /** * Get access information @@ -218,14 +216,4 @@ public function getStatusAction(Request $request): Access ], ); } - - protected function getQueryBuilder(Request $request): QueryBuilder - { - throw new Exception('Not implemented'); - } - - protected function getIdField(): string - { - throw new Exception('Not implemented'); - } } diff --git a/webapp/src/Controller/API/AccountController.php b/webapp/src/Controller/API/AccountController.php index 6ed27308f06..81f9f60008d 100644 --- a/webapp/src/Controller/API/AccountController.php +++ b/webapp/src/Controller/API/AccountController.php @@ -14,6 +14,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Security\Http\Attribute\IsGranted; +/** + * @extends AbstractRestController + */ #[Rest\Route('/contests/{cid}')] #[OA\Tag(name: 'Accounts')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -107,7 +110,8 @@ protected function getQueryBuilder(Request $request): QueryBuilder if ($request->query->has('team')) { $queryBuilder - ->andWhere('u.team = :team') + ->leftJoin('u.team', 't') + ->andWhere('t.externalid = :team') ->setParameter('team', $request->query->get('team')); } @@ -116,6 +120,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('u.%s', $this->eventLogService->externalIdFieldForEntity(User::class) ?? 'userid'); + return 'u.externalid'; } } diff --git a/webapp/src/Controller/API/AwardsController.php b/webapp/src/Controller/API/AwardsController.php index 45c90b5a57c..3dde1cea2de 100644 --- a/webapp/src/Controller/API/AwardsController.php +++ b/webapp/src/Controller/API/AwardsController.php @@ -11,7 +11,6 @@ use App\Service\ScoreboardService; use App\Utils\Scoreboard\Scoreboard; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\QueryBuilder; use Exception; use FOS\RestBundle\Controller\Annotations as Rest; use Nelmio\ApiDocBundle\Annotation\Model; @@ -27,7 +26,7 @@ #[OA\Response(ref: '#/components/responses/NotFound', response: 404)] #[OA\Response(ref: '#/components/responses/Unauthenticated', response: 401)] #[OA\Response(ref: '#/components/responses/InvalidResponse', response: 400)] -class AwardsController extends AbstractRestController +class AwardsController extends AbstractApiController { public function __construct( EntityManagerInterface $entityManager, @@ -106,14 +105,4 @@ protected function getContestAndScoreboard(Request $request): array return [$contest, $scoreboard]; } - - protected function getQueryBuilder(Request $request): QueryBuilder - { - throw new Exception('Not implemented'); - } - - protected function getIdField(): string - { - throw new Exception('Not implemented'); - } } diff --git a/webapp/src/Controller/API/BalloonController.php b/webapp/src/Controller/API/BalloonController.php index 1c5d1ad4e1b..02c4f780253 100644 --- a/webapp/src/Controller/API/BalloonController.php +++ b/webapp/src/Controller/API/BalloonController.php @@ -7,8 +7,6 @@ use App\Entity\Team; use App\Service\BalloonService; use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\QueryBuilder; -use Exception; use FOS\RestBundle\Controller\Annotations as Rest; use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; @@ -25,7 +23,7 @@ #[OA\Response(ref: '#/components/responses/InvalidResponse', response: 400)] #[OA\Response(ref: '#/components/responses/Unauthenticated', response: 401)] #[OA\Response(ref: '#/components/responses/Unauthorized', response: 403)] -class BalloonController extends AbstractRestController +class BalloonController extends AbstractApiController { /** * Get all the balloons for this contest. @@ -97,14 +95,4 @@ public function markDoneAction(int $balloonId, BalloonService $balloonService): { $balloonService->setDone($balloonId); } - - protected function getQueryBuilder(Request $request): QueryBuilder - { - throw new Exception('Not implemented'); - } - - protected function getIdField(): string - { - throw new Exception('Not implemented'); - } } diff --git a/webapp/src/Controller/API/ClarificationController.php b/webapp/src/Controller/API/ClarificationController.php index a9151cb2239..e3fa66263c0 100644 --- a/webapp/src/Controller/API/ClarificationController.php +++ b/webapp/src/Controller/API/ClarificationController.php @@ -22,6 +22,9 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Http\Attribute\IsGranted; +/** + * @extends AbstractRestController + */ #[Rest\Route('/contests/{cid}/clarifications')] #[OA\Tag(name: 'Clarifications')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -125,8 +128,7 @@ public function addAction( ->join('cp.problem', 'p') ->join('cp.contest', 'c') ->select('cp, c') - ->andWhere(sprintf('p.%s = :problem', - $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid')) + ->andWhere('p.externalid = :problem') ->andWhere('cp.contest = :contest') ->andWhere('cp.allowSubmit = 1') ->setParameter('problem', $problemId) @@ -148,8 +150,7 @@ public function addAction( $replyTo = $this->em->createQueryBuilder() ->from(Clarification::class, 'c') ->select('c') - ->andWhere(sprintf('c.%s = :clarification', - $this->eventLogService->externalIdFieldForEntity(Clarification::class) ?? 'clarid')) + ->andWhere('c.externalid = :clarification') ->andWhere('c.contest = :contest') ->setParameter('clarification', $replyToId) ->setParameter('contest', $contestId) @@ -166,15 +167,12 @@ public function addAction( // By default, use the team of the user $fromTeam = $this->isGranted('ROLE_API_WRITER') ? null : $this->dj->getUser()->getTeam(); if ($fromTeamId = $clarificationPost->fromTeamId) { - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $method = sprintf('get%s', ucfirst($idField)); - // If the user is an admin or API writer, allow it to specify the team if ($this->isGranted('ROLE_API_WRITER')) { - $fromTeam = $this->dj->loadTeam($idField, $fromTeamId, $contest); + $fromTeam = $this->dj->loadTeam($fromTeamId, $contest); } elseif (!$fromTeam) { throw new BadRequestHttpException('User does not belong to a team.'); - } elseif ((string)call_user_func([$fromTeam, $method]) !== (string)$fromTeamId) { + } elseif ($fromTeam->getExternalid() !== $fromTeamId) { throw new BadRequestHttpException('Can not create a clarification from a different team.'); } } elseif (!$this->isGranted('ROLE_API_WRITER') && !$fromTeam) { @@ -186,11 +184,9 @@ public function addAction( // By default, send to jury. $toTeam = null; if ($toTeamId = $clarificationPost->toTeamId) { - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - // If the user is an admin or API writer, allow it to specify the team. if ($this->isGranted('ROLE_API_WRITER')) { - $toTeam = $this->dj->loadTeam($idField, $toTeamId, $contest); + $toTeam = $this->dj->loadTeam($toTeamId, $contest); } else { throw new BadRequestHttpException('Can not create a clarification that is sent to a team.'); } @@ -284,7 +280,8 @@ protected function getQueryBuilder(Request $request): QueryBuilder ->leftJoin('clar.problem', 'p') ->select('clar, c, r, reply, p') ->andWhere('clar.contest = :cid') - ->setParameter('cid', $this->getContestId($request)); + ->setParameter('cid', $this->getContestId($request)) + ->orderBy('clar.clarid'); if (!$this->dj->checkrole('api_reader') && !$this->dj->checkrole('judgehost')) { @@ -310,6 +307,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('clar.%s', $this->eventLogService->externalIdFieldForEntity(Clarification::class) ?? 'clarid'); + return 'clar.externalid'; } } diff --git a/webapp/src/Controller/API/ContestController.php b/webapp/src/Controller/API/ContestController.php index f2de4dfcbe2..bea5588e4d5 100644 --- a/webapp/src/Controller/API/ContestController.php +++ b/webapp/src/Controller/API/ContestController.php @@ -26,6 +26,7 @@ use Metadata\MetadataFactoryInterface; use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -45,6 +46,9 @@ use Symfony\Component\Yaml\Yaml; use TypeError; +/** + * @extends AbstractRestController + */ #[Rest\Route('/contests')] #[OA\Tag(name: 'Contests')] #[OA\Parameter(ref: '#/components/parameters/strict')] @@ -60,6 +64,7 @@ public function __construct( ConfigurationService $config, EventLogService $eventLogService, protected readonly ImportExportService $importExportService, + protected readonly LoggerInterface $logger, protected readonly AssetUpdateService $assetUpdater ) { parent::__construct($entityManager, $dj, $config, $eventLogService); @@ -260,6 +265,121 @@ public function setBannerAction(Request $request, string $cid, ValidatorInterfac return new Response('', Response::HTTP_NO_CONTENT); } + /** + * Delete the problemset document for the given contest. + */ + #[IsGranted('ROLE_ADMIN')] + #[Rest\Delete('/{cid}/problemset', name: 'delete_contest_problemset')] + #[OA\Response(response: 204, description: 'Deleting problemset document succeeded')] + #[OA\Parameter(ref: '#/components/parameters/cid')] + public function deleteProblemsetAction(Request $request, string $cid): Response + { + $contest = $this->getContestAndCheckIfLocked($request, $cid); + $contest->setClearContestProblemset(true); + $contest->processContestProblemset(); + $this->em->flush(); + + $this->eventLogService->log('contests', $contest->getCid(), EventLogService::ACTION_UPDATE, + $contest->getCid()); + + return new Response('', Response::HTTP_NO_CONTENT); + } + + /** + * Set the problemset document for the given contest. + */ + #[IsGranted('ROLE_ADMIN')] + #[Rest\Post("/{cid}/problemset", name: 'post_contest_problemset')] + #[Rest\Put("/{cid}/problemset", name: 'put_contest_problemset')] + #[OA\RequestBody( + required: true, + content: new OA\MediaType( + mediaType: 'multipart/form-data', + schema: new OA\Schema( + required: ['problemset'], + properties: [ + new OA\Property( + property: 'problemset', + description: 'The problemset document to use, as either text/html, text/plain or application/pdf.', + type: 'string', + format: 'binary' + ), + ] + ) + ) + )] + #[OA\Response(response: 204, description: 'Setting problemset document succeeded')] + #[OA\Parameter(ref: '#/components/parameters/cid')] + public function setProblemsetAction(Request $request, string $cid, ValidatorInterface $validator): Response + { + $contest = $this->getContestAndCheckIfLocked($request, $cid); + + /** @var UploadedFile|null $problemset */ + $problemset = $request->files->get('problemset'); + if (!$problemset) { + return new JsonResponse(['title' => 'Validation failed', 'errors' => ['Please supply a problemset document']], Response::HTTP_BAD_REQUEST); + } + if (!in_array($problemset->getMimeType(), ['text/html', 'text/plain', 'application/pdf'])) { + return new JsonResponse(['title' => 'Validation failed', 'errors' => ['Invalid problemset document type']], Response::HTTP_BAD_REQUEST); + } + + $contest->setContestProblemsetFile($problemset); + + if ($errorResponse = $this->responseForErrors($validator->validate($contest), true)) { + return $errorResponse; + } + + $contest->processContestProblemset(); + $this->em->flush(); + + $this->eventLogService->log('contests', $contest->getCid(), EventLogService::ACTION_UPDATE, + $contest->getCid()); + + return new Response('', Response::HTTP_NO_CONTENT); + } + + /** + * Get the problemset document for the given contest. + */ + #[Rest\Get('/{cid}/problemset', name: 'contest_problemset')] + #[OA\Response( + response: 200, + description: 'Returns the given contest problemset document in PDF, HTML or TXT format', + content: [ + new OA\MediaType(mediaType: 'application/pdf'), + new OA\MediaType(mediaType: 'text/plain'), + new OA\MediaType(mediaType: 'text/html'), + ] + )] + #[OA\Parameter(ref: '#/components/parameters/cid')] + public function problemsetAction(Request $request, string $cid): Response + { + /** @var Contest|null $contest */ + $contest = $this->getQueryBuilder($request) + ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->setParameter('id', $cid) + ->getQuery() + ->getOneOrNullResult(); + + $hasAccess = $this->dj->checkrole('jury') || + $this->dj->checkrole('api_reader') || + $contest->getFreezeData()->started(); + + if (!$hasAccess) { + throw new AccessDeniedHttpException(); + } + + if ($contest === null) { + throw new NotFoundHttpException(sprintf('Object with ID \'%s\' not found', $cid)); + } + + if (!$contest->getContestProblemsetType()) { + throw new NotFoundHttpException(sprintf('Contest with ID \'%s\' has no problemset text', $cid)); + } + + return $contest->getContestProblemsetStreamedResponse(); + } + /** * Change the start time or unfreeze (thaw) time of the given contest. * @throws NonUniqueResultException @@ -297,7 +417,7 @@ public function changeStartTimeAction( if (!$request->request->has('start_time') && !$request->request->has('scoreboard_thaw_time')) { throw new BadRequestHttpException('Missing "start_time" or "scoreboard_thaw_time" in request.'); } - if ($request->request->get('id') != $contest->getApiId($this->eventLogService)) { + if ($request->request->get('id') != $contest->getExternalid()) { throw new BadRequestHttpException('Invalid "id" in request.'); } if ($request->request->has('start_time') && $request->request->has('scoreboard_thaw_time')) { @@ -533,7 +653,7 @@ public function getEventFeedAction( $response->headers->set('Content-Type', 'application/x-ndjson'); $response->setCallback(function () use ($format, $cid, $contest, $request, $since_id, $types, $strict, $stream, $metadataFactory, $kernel) { $lastUpdate = 0; - $lastIdSent = $since_id; + $lastIdSent = max(0, $since_id); // Don't try to look for event_id=0 $typeFilter = false; if ($types) { $typeFilter = explode(',', $types); @@ -596,39 +716,83 @@ public function getEventFeedAction( // Reload the contest as the above method will clear the entity manager. $contest = $this->getContestWithId($request, $cid); + $missingEventRetries = 0; while (true) { // Add missing state events that should have happened already. $this->eventLogService->addMissingStateEvents($contest); - $qb = $this->em->createQueryBuilder() + // We fetch *all* events after the last seen to check that + // we don't skip events that are committed out of order. + $q = $this->em->createQueryBuilder() ->from(Event::class, 'e') ->select('e') ->andWhere('e.eventid > :lastIdSent') ->setParameter('lastIdSent', $lastIdSent) - ->andWhere('e.contest = :cid') - ->setParameter('cid', $contest->getCid()) - ->orderBy('e.eventid', 'ASC'); - - if ($typeFilter !== false) { - $qb = $qb - ->andWhere('e.endpointtype IN (:types)') - ->setParameter('types', $typeFilter); - } - if (!$canViewAll) { - $restricted_types = ['judgements', 'runs', 'clarifications']; - if ($contest->getStarttime() === null || Utils::now() < $contest->getStarttime()) { - $restricted_types[] = 'problems'; + ->orderBy('e.eventid', 'ASC') + ->getQuery(); + + /** @var Event[] $events */ + $events = $q->getResult(); + + // Look for any missing sequential events and wait for them to + // be committed if so. + $missingEvents = false; + $expectedId = $lastIdSent + 1; + $lastFoundId = null; + foreach ($events as $event) { + if ($event->getEventid() !== $expectedId) { + $missingEvents = true; + $lastFoundId = $event->getEventid(); + break; } - $qb = $qb - ->andWhere('e.endpointtype NOT IN (:restricted_types)') - ->setParameter('restricted_types', $restricted_types); + $expectedId++; } + if ($missingEvents) { + if ($missingEventRetries == 0) { + $this->logger->info( + 'Detected missing events %d ... %d, waiting for these to appear', + [$expectedId, $lastFoundId-1] + ); + } + if (++$missingEventRetries < 10) { + usleep(100 * 1000); + continue; + } - $q = $qb->getQuery(); + // We've decided to permanently ignore these non-existing + // events for this connection. The wait for any + // non-committed events was long enough. + // + // There might be multiple non-existing events. Log the + // first consecutive gap of non-existing events. A consecutive + // gap is guaranteed since the events are ordered. + $this->logger->warning( + 'Waited too long for missing events %d ... %d, skipping', + [$expectedId, $lastFoundId-1] + ); + } + $missingEventRetries = 0; - /** @var Event[] $events */ - $events = $q->getResult(); + $numEventsSent = 0; foreach ($events as $event) { + // Filter out unwanted events + if ($event->getContest()->getCid() !== $contest->getCid()) { + continue; + } + if ($typeFilter !== false && + !in_array($event->getEndpointtype(), $typeFilter)) { + continue; + } + if (!$canViewAll) { + $restricted_types = ['judgements', 'runs', 'clarifications']; + if ($contest->getStarttime() === null || Utils::now() < $contest->getStarttime()) { + $restricted_types[] = 'problems'; + } + if (in_array($event->getEndpointtype(), $restricted_types)) { + continue; + } + } + $data = $event->getContent(); // Filter fields with specific access restrictions. if (!$canViewAll) { @@ -696,9 +860,17 @@ public function getEventFeedAction( flush(); $lastUpdate = Utils::now(); $lastIdSent = $event->getEventid(); + $numEventsSent++; + + if ($missingEvents && $event->getEventid() >= $lastFoundId) { + // The first event after the first gap has been emitted. Stop + // emitting events and restart the gap detection logic to find + // any potential gaps after this last emitted event. + break; + } } - if (count($events) == 0) { + if ($numEventsSent == 0) { if (!$stream) { break; } @@ -773,7 +945,7 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('c.%s', $this->eventLogService->externalIdFieldForEntity(Contest::class) ?? 'cid'); + return 'c.externalid'; } /** diff --git a/webapp/src/Controller/API/GeneralInfoController.php b/webapp/src/Controller/API/GeneralInfoController.php index d243b249f42..7aa0be7d148 100644 --- a/webapp/src/Controller/API/GeneralInfoController.php +++ b/webapp/src/Controller/API/GeneralInfoController.php @@ -137,8 +137,7 @@ public function getStatusAction(): array foreach ($contests as $contest) { $contestStats = $this->dj->getContestStats($contest); $result[] = new ExtendedContestStatus( - $this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_LOCAL - ? (string)$contest->getCid() : $contest->getExternalid(), + $contest->getExternalid(), $contestStats ); } @@ -163,6 +162,8 @@ public function getUserAction(): User /** * Get configuration variables. + * + * @return array> */ #[Rest\Get('/config')] #[OA\Response( @@ -202,6 +203,8 @@ public function getDatabaseConfigurationAction( /** * Update configuration variables. + * @return JsonResponse|array|string> + * * @throws NonUniqueResultException */ #[IsGranted('ROLE_ADMIN')] @@ -328,6 +331,8 @@ public function countryFlagAction( /** * Add a problem without linking it to a contest. + * + * @return array{problem_id: string, messages: array} */ #[IsGranted('ROLE_ADMIN')] #[Rest\Post('/problems')] @@ -379,16 +384,4 @@ public function addProblemAction(Request $request): array { return $this->importProblemService->importProblemFromRequest($request); } - - /** - * Get the field to use for getting contests by ID. - */ - protected function getContestIdField(): string - { - try { - return $this->eventLogService->externalIdFieldForEntity(Contest::class) ?? 'cid'; - } catch (Exception) { - return 'cid'; - } - } } diff --git a/webapp/src/Controller/API/GroupController.php b/webapp/src/Controller/API/GroupController.php index 93cbd76f04b..40c0ec23b64 100644 --- a/webapp/src/Controller/API/GroupController.php +++ b/webapp/src/Controller/API/GroupController.php @@ -3,6 +3,7 @@ namespace App\Controller\API; use App\DataTransferObject\TeamCategoryPost; +use App\DataTransferObject\TeamCategoryPut; use App\Entity\TeamCategory; use App\Service\ImportExportService; use Doctrine\ORM\NonUniqueResultException; @@ -16,6 +17,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +/** + * @extends AbstractRestController + */ #[Rest\Route('/contests/{cid}/groups')] #[OA\Tag(name: 'Groups')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -91,27 +95,76 @@ public function addAction( #[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)] TeamCategoryPost $teamCategoryPost, Request $request, - ImportExportService $importExport + ImportExportService $importExport, ): Response { $saved = []; - $importExport->importGroupsJson([ - [ - 'name' => $teamCategoryPost->name, - 'hidden' => $teamCategoryPost->hidden, - 'icpc_id' => $teamCategoryPost->icpcId, - 'sortorder' => $teamCategoryPost->sortorder, - 'color' => $teamCategoryPost->color, - 'allow_self_registration' => $teamCategoryPost->allowSelfRegistration, - ], - ], $message, $saved); + $groupData = [ + 'name' => $teamCategoryPost->name, + 'hidden' => $teamCategoryPost->hidden, + 'icpc_id' => $teamCategoryPost->icpcId, + 'sortorder' => $teamCategoryPost->sortorder, + 'color' => $teamCategoryPost->color, + 'allow_self_registration' => $teamCategoryPost->allowSelfRegistration, + ]; + $importExport->importGroupsJson([$groupData], $message, $saved); if (!empty($message)) { throw new BadRequestHttpException("Error while adding group: $message"); } + /** @var TeamCategory $group */ $group = $saved[0]; - $idField = $this->eventLogService->externalIdFieldForEntity(TeamCategory::class) ?? 'categoryid'; - $method = sprintf('get%s', ucfirst($idField)); - $id = call_user_func([$group, $method]); + $id = $group->getexternalid(); + + return $this->renderCreateData($request, $saved[0], 'group', $id); + } + + /** + * Update an existing group or create one with the given ID + */ + #[IsGranted('ROLE_API_WRITER')] + #[Rest\Put('/{id}')] + #[OA\RequestBody( + required: true, + content: [ + new OA\MediaType( + mediaType: 'multipart/form-data', + schema: new OA\Schema(ref: new Model(type: TeamCategoryPut::class)) + ), + ] + )] + #[OA\Response( + response: 201, + description: 'Returns the updated / added group', + content: new Model(type: TeamCategory::class) + )] + public function updateAction( + #[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)] + TeamCategoryPut $teamCategoryPut, + Request $request, + ImportExportService $importExport, + string $id, + ): Response { + $saved = []; + $groupData = [ + 'id' => $teamCategoryPut->id, + 'name' => $teamCategoryPut->name, + 'hidden' => $teamCategoryPut->hidden, + 'icpc_id' => $teamCategoryPut->icpcId, + 'sortorder' => $teamCategoryPut->sortorder, + 'color' => $teamCategoryPut->color, + 'allow_self_registration' => $teamCategoryPut->allowSelfRegistration, + ]; + if ($id !== $teamCategoryPut->id) { + throw new BadRequestHttpException('ID in URL does not match ID in payload'); + } + $importExport->importGroupsJson([$groupData], $message, $saved); + if (!empty($message)) { + throw new BadRequestHttpException("Error while adding group: $message"); + } + + /** @var TeamCategory $group */ + $group = $saved[0]; + $id = $group->getexternalid(); return $this->renderCreateData($request, $saved[0], 'group', $id); } @@ -135,6 +188,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('c.%s', $this->eventLogService->externalIdFieldForEntity(TeamCategory::class) ?? 'categoryid'); + return 'c.externalid'; } } diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index 5115f69c7f5..f9252cc5d33 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -113,6 +113,8 @@ public function getJudgehostsAction( /** * Add a new judgehost to the list of judgehosts. * Also restarts (and returns) unfinished judgings. + * + * @return array * @throws NonUniqueResultException */ #[IsGranted('ROLE_JUDGEHOST')] @@ -192,6 +194,8 @@ public function createJudgehostAction(Request $request): array /** * Update the configuration of the given judgehost. + * + * @return Judgehost[] */ #[IsGranted('ROLE_JUDGEHOST')] #[Rest\Put('/{hostname}')] @@ -284,7 +288,9 @@ public function updateJudgingAction( ): void { $judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]); if (!$judgehost) { - throw new BadRequestHttpException("Who are you and why are you sending us any data?"); + throw new BadRequestHttpException( + 'Register yourself first. You (' . $hostname . ') are not known to us yet.' + ); } $judgingRun = $this->em->getRepository(JudgingRun::class)->findOneBy(['judgetaskid' => $judgetaskid]); @@ -377,15 +383,18 @@ public function updateJudgingAction( } } } else { + $compileMetadata = $request->request->get('compile_metadata'); $this->em->wrapInTransaction(function () use ( $judgehost, $judging, $query, - $output_compile + $output_compile, + $compileMetadata ) { if ($judging->getOutputCompile() === null) { $judging ->setOutputCompile($output_compile) + ->setCompileMetadata(base64_decode($compileMetadata)) ->setResult(Judging::RESULT_COMPILER_ERROR) ->setEndtime(Utils::now()); $this->em->flush(); @@ -422,7 +431,7 @@ public function updateJudgingAction( ->setJudging($judging) ->setContest($judging->getContest()) ->setDescription('Compilation results are different for j' . $judging->getJudgingid()) - ->setJudgehostlog('New compilation output: ' . $output_compile) + ->setJudgehostlog(base64_encode('New compilation output: ' . $output_compile)) ->setTime(Utils::now()) ->setDisabled($disabled); $this->em->persist($error); @@ -1184,6 +1193,8 @@ public function getFilesAction( /** * Get version commands for a given compile script. + * + * @return array{compiler_version_command?: string, runner_version_command?: string} */ #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_JUDGEHOST')"))] #[Rest\Get('/get_version_commands/{judgetaskid<\d+>}')] @@ -1224,6 +1235,9 @@ public function getVersionCommands(string $judgetaskid): array return $ret; } + /** + * @return array{} + */ #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_JUDGEHOST')"))] #[Rest\Put('/check_versions/{judgetaskid}')] #[OA\Response( diff --git a/webapp/src/Controller/API/JudgementController.php b/webapp/src/Controller/API/JudgementController.php index a0f3cd7b883..ca6530efbc8 100644 --- a/webapp/src/Controller/API/JudgementController.php +++ b/webapp/src/Controller/API/JudgementController.php @@ -18,6 +18,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Attribute\IsGranted; +/** + * @extends AbstractRestController + */ #[Rest\Route('/')] #[OA\Tag(name: 'Judgements')] #[OA\Parameter(ref: '#/components/parameters/cid')] diff --git a/webapp/src/Controller/API/JudgementTypeController.php b/webapp/src/Controller/API/JudgementTypeController.php index c8b02b54d1c..fe8588c7335 100644 --- a/webapp/src/Controller/API/JudgementTypeController.php +++ b/webapp/src/Controller/API/JudgementTypeController.php @@ -17,7 +17,7 @@ #[OA\Parameter(ref: '#/components/parameters/strict')] #[OA\Response(ref: '#/components/responses/InvalidResponse', response: 400)] #[OA\Response(ref: '#/components/responses/Unauthenticated', response: 401)] -class JudgementTypeController extends AbstractRestController +class JudgementTypeController extends AbstractApiController { /** * Get all the judgement types for this contest. @@ -112,14 +112,4 @@ protected function getJudgementTypes(array $filteredOn = null): array } return $result; } - - protected function getQueryBuilder(Request $request): QueryBuilder - { - throw new Exception('Not implemented'); - } - - protected function getIdField(): string - { - throw new Exception('Not implemented'); - } } diff --git a/webapp/src/Controller/API/LanguageController.php b/webapp/src/Controller/API/LanguageController.php index 3128113d5c9..0415d4120bb 100644 --- a/webapp/src/Controller/API/LanguageController.php +++ b/webapp/src/Controller/API/LanguageController.php @@ -17,6 +17,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; use ZipArchive; +/** + * @extends AbstractRestController + */ #[Rest\Route('/')] #[OA\Tag(name: 'Languages')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -145,12 +148,11 @@ public function configureLanguagesAction(Request $request): Response $language->setAllowSubmit(false); } - $idField = $this->eventLogService->externalIdFieldForEntity(Language::class) ?? 'langid'; foreach ($newLanguages as $language) { /** @var Language $language */ $lang_id = $language['id']; $lang = $this->em->getRepository(Language::class)->findOneBy( - [$idField => $lang_id] + ['externalid' => $lang_id] ); if (!$lang) { // TODO: Decide how to handle this case, either erroring out or creating a new language. @@ -222,6 +224,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('lang.%s', $this->eventLogService->externalIdFieldForEntity(Language::class) ?? 'langid'); + return 'lang.externalid'; } } diff --git a/webapp/src/Controller/API/MetricsController.php b/webapp/src/Controller/API/MetricsController.php index 2024e0be3ff..c3092f9633a 100644 --- a/webapp/src/Controller/API/MetricsController.php +++ b/webapp/src/Controller/API/MetricsController.php @@ -106,6 +106,9 @@ public function prometheusAction(): Response new SubmissionRestriction(visible: true) ); foreach ($submissionCounts as $kind => $count) { + if (!array_key_exists('submissions_' . $kind, $m)) { + continue; + } $m['submissions_' . $kind]->set((int)$count, $labels); } // Get team submission stats for the contest. diff --git a/webapp/src/Controller/API/OrganizationController.php b/webapp/src/Controller/API/OrganizationController.php index 757afb72491..68137d99a10 100644 --- a/webapp/src/Controller/API/OrganizationController.php +++ b/webapp/src/Controller/API/OrganizationController.php @@ -25,6 +25,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Validator\Validator\ValidatorInterface; +/** + * @extends AbstractRestController + */ #[Rest\Route('/')] #[OA\Tag(name: 'Organizations')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -107,7 +110,7 @@ public function logoAction(Request $request, string $id): Response { /** @var TeamAffiliation|null $teamAffiliation */ $teamAffiliation = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('ta.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -138,7 +141,7 @@ public function deleteLogoAction(Request $request, string $id): Response $contestId = null; /** @var TeamAffiliation|null $teamAffiliation */ $teamAffiliation = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('ta.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -190,7 +193,7 @@ public function setLogoAction(Request $request, string $id, ValidatorInterface $ { /** @var TeamAffiliation|null $teamAffiliation */ $teamAffiliation = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('ta.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -263,10 +266,9 @@ public function addAction( throw new BadRequestHttpException("Error while adding organization: $message"); } + /** @var TeamAffiliation $organization */ $organization = $saved[0]; - $idField = $this->eventLogService->externalIdFieldForEntity(TeamAffiliation::class) ?? 'affilid'; - $method = sprintf('get%s', ucfirst($idField)); - $id = call_user_func([$organization, $method]); + $id = $organization->getExternalid(); return $this->renderCreateData($request, $saved[0], 'organization', $id); } @@ -293,6 +295,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('ta.%s', $this->eventLogService->externalIdFieldForEntity(TeamAffiliation::class) ?? 'affilid'); + return 'ta.externalid'; } } diff --git a/webapp/src/Controller/API/ProblemController.php b/webapp/src/Controller/API/ProblemController.php index 31922076b12..ecfab13749f 100644 --- a/webapp/src/Controller/API/ProblemController.php +++ b/webapp/src/Controller/API/ProblemController.php @@ -30,6 +30,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Yaml\Yaml; +/** + * @extends AbstractRestController + */ #[Rest\Route('/contests/{cid}/problems')] #[OA\Tag(name: 'Problems')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -54,6 +57,7 @@ public function __construct( /** * Add one or more problems. * + * @return int[] * @throws BadRequestHttpException * @throws NonUniqueResultException */ @@ -98,10 +102,14 @@ public function addProblemsAction(Request $request): array // Note: we read the JSON as YAML, since any JSON is also YAML and this allows us // to import files with YAML inside them that match the JSON format $data = Yaml::parseFile($file->getRealPath(), Yaml::PARSE_DATETIME); - if ($this->importExportService->importProblemsData($contest, $data, $ids)) { + if ($this->importExportService->importProblemsData($contest, $data, $ids, $messages)) { return $ids; } - throw new BadRequestHttpException("Error while adding problems"); + $message = "Error while adding problems"; + if (!empty($messages)) { + $message .= ': ' . $this->dj->jsonEncode($messages); + } + throw new BadRequestHttpException($message); } /** @@ -150,8 +158,7 @@ public function listAction(Request $request): Response if ($contestProblem instanceof ContestProblemWrapper) { $contestProblem = $contestProblem->getContestProblem(); } - $probid = $this->getIdField() === 'p.probid' ? $contestProblem->getProbid() : $contestProblem->getExternalId(); - if (in_array($probid, $ids)) { + if (in_array($contestProblem->getExternalId(), $ids)) { $objects[] = $item; } } @@ -166,6 +173,7 @@ public function listAction(Request $request): Response /** * Add a problem to this contest. + * @return array{problem_id: string, messages: array} * @throws NonUniqueResultException */ #[IsGranted('ROLE_ADMIN')] @@ -374,8 +382,7 @@ public function singleAction(Request $request, string $id): Response if ($contestProblem instanceof ContestProblemWrapper) { $contestProblem = $contestProblem->getContestProblem(); } - $probid = $this->getIdField() === 'p.probid' ? $contestProblem->getProbid() : $contestProblem->getExternalId(); - if ($probid == $id) { + if ($contestProblem->getExternalId() == $id) { $object = $item; break; } @@ -402,7 +409,7 @@ public function singleAction(Request $request, string $id): Response public function statementAction(Request $request, string $id): Response { $queryBuilder = $this->getQueryBuilder($request) - ->leftJoin('p.problemTextContent', 'content') + ->leftJoin('p.problemStatementContent', 'content') ->addSelect('content') ->setParameter('id', $id) ->andWhere(sprintf('%s = :id', $this->getIdField())); @@ -420,11 +427,11 @@ public function statementAction(Request $request, string $id): Response /** @var ContestProblem $contestProblem */ $contestProblem = $contestProblemData[0]; - if ($contestProblem->getProblem()->getProblemtextType() !== 'pdf') { + if ($contestProblem->getProblem()->getProblemstatementType() !== 'pdf') { throw new NotFoundHttpException(sprintf('Problem with ID \'%s\' has no PDF statement', $id)); } - return $contestProblem->getProblem()->getProblemTextStreamedResponse(); + return $contestProblem->getProblem()->getProblemStatementStreamedResponse(); } protected function getQueryBuilder(Request $request): QueryBuilder @@ -454,12 +461,12 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('p.%s', $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid'); + return 'p.externalid'; } /** * Transform the given object before returning it from the API. - * @param array $object + * @param array{0: ContestProblem, testdatacount: int} $object */ public function transformObject($object): ContestProblem|ContestProblemWrapper { diff --git a/webapp/src/Controller/API/RunController.php b/webapp/src/Controller/API/RunController.php index 9d9d503aac5..c73ff8780d3 100644 --- a/webapp/src/Controller/API/RunController.php +++ b/webapp/src/Controller/API/RunController.php @@ -19,6 +19,9 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Http\Attribute\IsGranted; +/** + * @extends AbstractRestController + */ #[Rest\Route('/contests/{cid}/runs')] #[OA\Tag(name: 'Runs')] #[OA\Parameter(ref: '#/components/parameters/cid')] diff --git a/webapp/src/Controller/API/ScoreboardController.php b/webapp/src/Controller/API/ScoreboardController.php index 2d6f3162aa8..b3ba4021d76 100644 --- a/webapp/src/Controller/API/ScoreboardController.php +++ b/webapp/src/Controller/API/ScoreboardController.php @@ -17,13 +17,12 @@ use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\QueryBuilder; -use Exception; use FOS\RestBundle\Controller\Annotations as Rest; use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; #[Rest\Route('/contests/{cid}/scoreboard')] #[OA\Tag(name: 'Scoreboard')] @@ -33,7 +32,7 @@ #[OA\Response(ref: '#/components/responses/Unauthenticated', response: 401)] #[OA\Response(ref: '#/components/responses/Unauthorized', response: 403)] #[OA\Response(ref: '#/components/responses/NotFound', response: 404)] -class ScoreboardController extends AbstractRestController +class ScoreboardController extends AbstractApiController { public function __construct( EntityManagerInterface $entityManager, @@ -108,6 +107,10 @@ public function getScoreboardAction( #[MapQueryParameter] bool $strict = false, ): Scoreboard { + if (!$this->config->get('enable_ranking') && !$this->dj->checkrole('jury')) { + throw new BadRequestHttpException('Scoreboard is not available.'); + } + $filter = new Filter(); if ($category) { $filter->categories = [$category]; @@ -189,7 +192,7 @@ public function getScoreboardAction( $contestProblem = $scoreboard->getProblems()[$problemId]; $problem = new Problem( label: $contestProblem->getShortname(), - problemId: $contestProblem->getApiId($this->eventLogService), + problemId: $contestProblem->getProblem()->getExternalid(), numJudged: $matrixItem->numSubmissions, numPending: $matrixItem->numSubmissionsPending, solved: $matrixItem->isCorrect, @@ -214,7 +217,7 @@ public function getScoreboardAction( $row = new Row( rank: $teamScore->rank, - teamId: $teamScore->team->getApiId($this->eventLogService), + teamId: $teamScore->team->getExternalid(), score: $score, problems: $problems, ); @@ -224,14 +227,4 @@ public function getScoreboardAction( return $results; } - - protected function getQueryBuilder(Request $request): QueryBuilder - { - throw new Exception('Not implemented'); - } - - protected function getIdField(): string - { - throw new Exception('Not implemented'); - } } diff --git a/webapp/src/Controller/API/SubmissionController.php b/webapp/src/Controller/API/SubmissionController.php index e8ba3c8e283..292efdf2349 100644 --- a/webapp/src/Controller/API/SubmissionController.php +++ b/webapp/src/Controller/API/SubmissionController.php @@ -34,6 +34,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +/** + * @extends AbstractRestController + */ #[Rest\Route('/')] #[OA\Tag(name: 'Submissions')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -133,20 +136,18 @@ public function addSubmissionAction( // By default, use the user and team of the user. $user = $this->dj->getUser(); $team = $user->getTeam(); - if ($teamId = $addSubmission->teamId) { - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $method = sprintf('get%s', ucfirst($idField)); - + $teamId = $addSubmission->teamId; + if ($teamId) { // If the user is an admin or API writer, allow it to specify the team. if ($this->isGranted('ROLE_API_WRITER')) { /** @var Contest $contest */ $contest = $this->em->getRepository(Contest::class)->find($this->getContestId($request)); /** @var Team $team */ - $team = $this->dj->loadTeam($idField, $teamId, $contest); + $team = $this->dj->loadTeam($teamId, $contest); $user = $team->getUsers()->first() ?: null; } elseif (!$team) { throw new BadRequestHttpException('User does not belong to a team.'); - } elseif ((string)call_user_func([$team, $method]) !== (string)$teamId) { + } elseif ($team->getExternalid() !== $teamId) { throw new BadRequestHttpException('Can not submit for a different team.'); } } elseif (!$team) { @@ -184,8 +185,7 @@ public function addSubmissionAction( ->join('cp.problem', 'p') ->join('cp.contest', 'c') ->select('cp, c') - ->andWhere(sprintf('p.%s = :problem', - $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid')) + ->andWhere('p.externalid = :problem') ->andWhere('cp.contest = :contest') ->andWhere('cp.allowSubmit = 1') ->setParameter('problem', $problemId) @@ -203,8 +203,7 @@ public function addSubmissionAction( $language = $this->em->createQueryBuilder() ->from(Language::class, 'lang') ->select('lang') - ->andWhere(sprintf('lang.%s = :language', - $this->eventLogService->externalIdFieldForEntity(Language::class) ?? 'langid')) + ->andWhere('lang.externalid = :language') ->andWhere('lang.allowSubmit = 1') ->setParameter('language', $languageId) ->getQuery() @@ -252,7 +251,7 @@ public function addSubmissionAction( $existingSubmission = $this->em->createQueryBuilder() ->from(Submission::class, 's') ->select('s') - ->andWhere('(s.externalid IS NULL AND s.submitid = :submitid) OR s.externalid = :submitid') + ->andWhere('s.externalid = :submitid') ->andWhere('s.contest = :contest') ->setParameter('submitid', $submissionId) ->setParameter('contest', $problem->getContest()) @@ -369,12 +368,7 @@ public function getSubmissionFilesAction(Request $request, string $id): Response ->select('s, f') ->setParameter('id', $id); - $idField = $this->getIdField(); - if ($idField === 's.submitid') { - $queryBuilder->andWhere('(s.externalid IS NULL AND s.submitid = :id) OR s.externalid = :id'); - } else { - $queryBuilder->andWhere(sprintf('%s = :id', $idField)); - } + $queryBuilder->andWhere('s.externalid = :id'); /** @var Submission[] $submissions */ $submissions = $queryBuilder->getQuery()->getResult(); @@ -484,6 +478,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('s.%s', $this->eventLogService->externalIdFieldForEntity(Submission::class) ?? 'submitid'); + return 's.externalid'; } } diff --git a/webapp/src/Controller/API/TeamController.php b/webapp/src/Controller/API/TeamController.php index 63374228e6d..ee7fb2c5e3b 100644 --- a/webapp/src/Controller/API/TeamController.php +++ b/webapp/src/Controller/API/TeamController.php @@ -26,6 +26,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Validator\Validator\ValidatorInterface; +/** + * @extends AbstractRestController + */ #[Rest\Route('/')] #[OA\Tag(name: 'Teams')] #[OA\Parameter(ref: '#/components/parameters/cid')] @@ -81,6 +84,9 @@ public function __construct( )] public function listAction(Request $request): Response { + if (!$this->config->get('enable_ranking') && !$this->dj->checkrole('jury')) { + throw new BadRequestHttpException("teams list not available."); + } return parent::performListAction($request); } @@ -98,6 +104,9 @@ public function listAction(Request $request): Response #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response { + if (!$this->config->get('enable_ranking') && !$this->dj->checkrole('jury')) { + throw new BadRequestHttpException("team not available."); + } return parent::performSingleAction($request, $id); } @@ -118,9 +127,12 @@ public function singleAction(Request $request, string $id): Response #[OA\Parameter(ref: '#/components/parameters/id')] public function photoAction(Request $request, string $id): Response { + if (!$this->config->get('enable_ranking') && !$this->dj->checkrole('jury')) { + throw new BadRequestHttpException("team photo not available."); + } /** @var Team|null $team */ $team = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('t.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -150,7 +162,7 @@ public function deletePhotoAction(Request $request, string $id): Response $contestId = null; /** @var Team|null $team */ $team = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('t.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -202,7 +214,7 @@ public function setPhotoAction(Request $request, string $id, ValidatorInterface { /** @var Team|null $team */ $team = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('t.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -281,10 +293,9 @@ public function addAction( throw new BadRequestHttpException("Error while adding team: $message"); } + /** @var Team $team */ $team = $saved[0]; - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $method = sprintf('get%s', ucfirst($idField)); - $id = call_user_func([$team, $method]); + $id = $team->getExternalid(); return $this->renderCreateData($request, $saved[0], 'team', $id); } @@ -329,6 +340,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('t.%s', $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'); + return 't.externalid'; } } diff --git a/webapp/src/Controller/API/UserController.php b/webapp/src/Controller/API/UserController.php index 2e3dc496a04..00d89a4dbb8 100644 --- a/webapp/src/Controller/API/UserController.php +++ b/webapp/src/Controller/API/UserController.php @@ -3,6 +3,7 @@ namespace App\Controller\API; use App\DataTransferObject\AddUser; +use App\DataTransferObject\UpdateUser; use App\Entity\Role; use App\Entity\Team; use App\Entity\User; @@ -24,6 +25,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +/** + * @extends AbstractRestController + */ #[Rest\Route('/users', defaults: ['_format' => 'json'])] #[OA\Tag(name: 'Users')] #[OA\Response(ref: '#/components/responses/InvalidResponse', response: 400)] @@ -64,7 +68,7 @@ public function __construct( description: 'The groups.json files to import.', type: 'string', format: 'binary' - ) + ), ] ) ) @@ -106,7 +110,7 @@ public function addGroupsAction(Request $request): string property: 'json', description: 'The organizations.json files to import.', type: 'string', - format: 'binary') + format: 'binary'), ] ) ) @@ -150,7 +154,7 @@ public function addOrganizationsAction(Request $request): string description: 'The teams.json files to import.', type: 'string', format: 'binary' - ) + ), ] ) ) @@ -205,7 +209,7 @@ public function addTeamsAction(Request $request): string description: 'The accounts.yaml files to import.', type: 'string', format: 'binary' - ) + ), ] ) ) @@ -294,7 +298,7 @@ public function singleAction(Request $request, string $id): Response new OA\MediaType( mediaType: 'multipart/form-data', schema: new OA\Schema(ref: new Model(type: AddUser::class)) - ) + ), ] )] #[OA\Response( @@ -307,11 +311,53 @@ public function addAction( AddUser $addUser, Request $request ): Response { + return $this->addOrUpdateUser($addUser, $request); + } + + /** + * Update an existing User or create one with the given ID + */ + #[IsGranted('ROLE_API_WRITER')] + #[Rest\Put('/{id}')] + #[OA\RequestBody( + required: true, + content: [ + new OA\MediaType( + mediaType: 'multipart/form-data', + schema: new OA\Schema(ref: new Model(type: UpdateUser::class)) + ), + ] + )] + #[OA\Response( + response: 201, + description: 'Returns the added user', + content: new Model(type: User::class) + )] + public function updateAction( + #[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)] + UpdateUser $updateUser, + Request $request + ): Response { + return $this->addOrUpdateUser($updateUser, $request); + } + + protected function addOrUpdateUser(AddUser $addUser, Request $request): Response + { + if ($addUser instanceof UpdateUser && !$addUser->id) { + throw new BadRequestHttpException('`id` field is required'); + } + if ($this->em->getRepository(User::class)->findOneBy(['username' => $addUser->username])) { throw new BadRequestHttpException(sprintf("User %s already exists", $addUser->username)); } $user = new User(); + if ($addUser instanceof UpdateUser) { + $existingUser = $this->em->getRepository(User::class)->findOneBy(['externalid' => $addUser->id]); + if ($existingUser) { + $user = $existingUser; + } + } $user ->setUsername($addUser->username) ->setName($addUser->name) @@ -320,13 +366,16 @@ public function addAction( ->setPlainPassword($addUser->password) ->setEnabled($addUser->enabled ?? true); + if ($addUser instanceof UpdateUser) { + $user->setExternalid($addUser->id); + } + if ($addUser->teamId) { /** @var Team|null $team */ $team = $this->em->createQueryBuilder() ->from(Team::class, 't') ->select('t') - ->andWhere(sprintf('t.%s = :team', - $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid')) + ->andWhere('t.externalid = :team') ->setParameter('team', $addUser->teamId) ->getQuery() ->getOneOrNullResult(); @@ -383,6 +432,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('u.%s', $this->eventLogService->externalIdFieldForEntity(User::class) ?? 'userid'); + return 'u.externalid'; } } diff --git a/webapp/src/Controller/BaseController.php b/webapp/src/Controller/BaseController.php index f206204d91f..4a75c61a6f3 100644 --- a/webapp/src/Controller/BaseController.php +++ b/webapp/src/Controller/BaseController.php @@ -2,9 +2,12 @@ namespace App\Controller; +use App\Doctrine\ExternalIdAlreadyExistsException; use App\Entity\BaseApiEntity; +use App\Entity\CalculatedExternalIdBasedOnRelatedFieldInterface; use App\Entity\Contest; use App\Entity\ContestProblem; +use App\Entity\ExternalIdFromInternalIdInterface; use App\Entity\Problem; use App\Entity\RankCache; use App\Entity\ScoreCache; @@ -20,6 +23,8 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -37,6 +42,13 @@ */ abstract class BaseController extends AbstractController { + public function __construct( + protected readonly EntityManagerInterface $em, + protected readonly EventLogService $eventLog, + protected readonly DOMJudgeService $dj, + protected readonly KernelInterface $kernel, + ) {} + /** * Check whether the referrer in the request is of the current application. */ @@ -53,8 +65,11 @@ protected function isLocalReferer(RouterInterface $router, Request $request): bo /** * Check whether the given referer is local. */ - protected function isLocalRefererUrl(RouterInterface $router, string $referer, string $prefix): bool - { + protected function isLocalRefererUrl( + RouterInterface $router, + string $referer, + string $prefix + ): bool { if (str_starts_with($referer, $prefix)) { $path = substr($referer, strlen($prefix)); if (($questionMark = strpos($path, '?')) !== false) { @@ -79,8 +94,11 @@ protected function isLocalRefererUrl(RouterInterface $router, string $referer, s /** * Redirect to the referrer if it is a known (local) route, otherwise redirect to the given URL. */ - protected function redirectToLocalReferrer(RouterInterface $router, Request $request, string $defaultUrl): RedirectResponse - { + protected function redirectToLocalReferrer( + RouterInterface $router, + Request $request, + string $defaultUrl + ): RedirectResponse { if ($this->isLocalReferer($router, $request)) { return $this->redirect($request->headers->get('referer')); } @@ -92,66 +110,71 @@ protected function redirectToLocalReferrer(RouterInterface $router, Request $req * Save the given entity, adding an eventlog and auditlog entry. */ protected function saveEntity( - EntityManagerInterface $entityManager, - EventLogService $eventLogService, - DOMJudgeService $DOMJudgeService, object $entity, mixed $id, bool $isNewEntity ): void { $auditLogType = Utils::tableForEntity($entity); - $entityManager->persist($entity); - $entityManager->flush(); + // Call the prePersist lifecycle callbacks. + // This used to work in preUpdate, but Doctrine has deprecated that feature. + // See https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/reference/events.html#events-overview. + $metadata = $this->em->getClassMetadata($entity::class); + foreach ($metadata->lifecycleCallbacks['prePersist'] ?? [] as $prePersistMethod) { + $entity->$prePersistMethod(); + } + + $this->em->persist($entity); + $this->em->flush(); // If we have no ID but we do have a Doctrine entity, automatically // get the primary key if possible. if ($id === null) { try { - $metadata = $entityManager->getClassMetadata($entity::class); + $metadata = $this->em->getClassMetadata($entity::class); if (count($metadata->getIdentifierColumnNames()) === 1) { $primaryKey = $metadata->getIdentifierColumnNames()[0]; - $accessor = PropertyAccess::createPropertyAccessor(); - $id = $accessor->getValue($entity, $primaryKey); + $accessor = PropertyAccess::createPropertyAccessor(); + $id = $accessor->getValue($entity, $primaryKey); } } catch (MappingException) { // Entity is not actually a Doctrine entity, ignore. } } - if ($endpoint = $eventLogService->endpointForEntity($entity)) { - foreach ($this->contestsForEntity($entity, $DOMJudgeService) as $contest) { - $eventLogService->log($endpoint, $id, - $isNewEntity ? EventLogService::ACTION_CREATE : EventLogService::ACTION_UPDATE, - $contest->getCid()); + if ($endpoint = $this->eventLog->endpointForEntity($entity)) { + foreach ($this->contestsForEntity($entity) as $contest) { + $this->eventLog->log($endpoint, $id, + $isNewEntity ? EventLogService::ACTION_CREATE : EventLogService::ACTION_UPDATE, + $contest->getCid()); } } - $DOMJudgeService->auditlog($auditLogType, $id, $isNewEntity ? 'added' : 'updated'); + $this->dj->auditlog($auditLogType, $id, $isNewEntity ? 'added' : 'updated'); } /** * Helper function to get the database structure for an object. * * @param string[] $files + * * @return array> */ - protected function getDatabaseRelations(array $files, EntityManagerInterface $entityManager): array - { + protected function getDatabaseRelations(array $files): array { $relations = []; foreach ($files as $file) { - $parts = explode('/', $file); + $parts = explode('/', $file); $shortClass = str_replace('.php', '', $parts[count($parts) - 1]); - $class = sprintf('App\\Entity\\%s', $shortClass); + $class = sprintf('App\\Entity\\%s', $shortClass); if (class_exists($class) && !in_array($class, - [RankCache::class, ScoreCache::class, BaseApiEntity::class])) { - $metadata = $entityManager->getClassMetadata($class); + [RankCache::class, ScoreCache::class, BaseApiEntity::class])) { + $metadata = $this->em->getClassMetadata($class); $tableRelations = []; foreach ($metadata->getAssociationMappings() as $associationMapping) { if (isset($associationMapping['joinColumns']) && count($associationMapping['joinColumns']) === 1) { foreach ($associationMapping['joinColumns'] as $joinColumn) { - $type = $joinColumn['onDelete'] ?? null; + $type = $joinColumn['onDelete'] ?? null; $tableRelations[$associationMapping['fieldName']] = [ 'target' => $associationMapping['targetEntity'], 'targetColumn' => $joinColumn['referencedColumnName'], @@ -172,13 +195,7 @@ protected function getDatabaseRelations(array $files, EntityManagerInterface $en * * @param int[] $primaryKeyData */ - protected function commitDeleteEntity( - object $entity, - DOMJudgeService $DOMJudgeService, - EntityManagerInterface $entityManager, - array $primaryKeyData, - EventLogService $eventLogService - ): void { + protected function commitDeleteEntity(object $entity, array $primaryKeyData): void { // Used to remove data from the rank and score caches. $teamId = null; if ($entity instanceof Team) { @@ -187,7 +204,7 @@ protected function commitDeleteEntity( // Get the contests to trigger the event for. We do this before // deleting the entity, since linked data might have vanished. - $contestsForEntity = $this->contestsForEntity($entity, $DOMJudgeService); + $contestsForEntity = $this->contestsForEntity($entity); $cid = null; // Remember the cid to use it in the EventLog later. @@ -197,11 +214,11 @@ protected function commitDeleteEntity( // Add an audit log entry. $auditLogType = Utils::tableForEntity($entity); - $DOMJudgeService->auditlog($auditLogType, implode(', ', $primaryKeyData), 'deleted'); + $this->dj->auditlog($auditLogType, implode(', ', $primaryKeyData), 'deleted'); // Trigger the delete event. We need to do this before deleting the entity to make // sure we can still find the entity in the table. - if ($endpoint = $eventLogService->endpointForEntity($entity)) { + if ($endpoint = $this->eventLog->endpointForEntity($entity)) { foreach ($contestsForEntity as $contest) { // When the $entity is a contest it has no id anymore after the EntityManager->remove // for this reason we either remember it or check all other contests and use their cid. @@ -213,14 +230,14 @@ protected function commitDeleteEntity( $dataId = $entity->getProbid(); } // TODO: cascade deletes. Maybe use getDependentEntities()? - $eventLogService->log($endpoint, $dataId, + $this->eventLog->log($endpoint, $dataId, EventLogService::ACTION_DELETE, $cid, null, null, false); } } // Now actually delete the entity. - $entityManager->wrapInTransaction(function () use ($entityManager, $entity) { + $this->em->wrapInTransaction(function () use ($entity) { if ($entity instanceof Problem) { // Deleting a problem is a special case: // Its dependent tables do not form a tree (but something like a diamond shape), @@ -232,7 +249,7 @@ protected function commitDeleteEntity( // See also https://github.com/DOMjudge/domjudge/issues/243 and associated commits. // First delete judging_runs. - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE jr FROM judging_run jr INNER JOIN judging j ON jr.judgingid = j.judgingid INNER JOIN submission s ON j.submitid = s.submitid @@ -241,32 +258,32 @@ protected function commitDeleteEntity( ); // Then delete submissions which will cascade to judging, judgeTasks and queueTasks. - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM submission WHERE probid = :probid', ['probid' => $entity->getProbid()] ); // Lastly, delete internal errors that are "connected" to this problem. $disabledJson = '{"kind":"problem","probid":' . $entity->getProbid() . '}'; - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM internal_error WHERE disabled = :disabled', ['disabled' => $disabledJson] ); - $entityManager->clear(); - $entity = $entityManager->getRepository(Problem::class)->find($entity->getProbid()); + $this->em->clear(); + $entity = $this->em->getRepository(Problem::class)->find($entity->getProbid()); } - $entityManager->remove($entity); + $this->em->remove($entity); }); if ($entity instanceof Team) { // No need to do this in a transaction, since the chance of a team // with same ID being created at the same time is negligible. - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM scorecache WHERE teamid = :teamid', ['teamid' => $teamId] ); - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM rankcache WHERE teamid = :teamid', ['teamid' => $teamId] ); @@ -276,32 +293,29 @@ protected function commitDeleteEntity( /** * @param Object[] $entities * @param array> $relations + * * @return array{0: bool, 1: array, 2: string[]} */ - protected function buildDeleteTree( - array $entities, - array $relations, - EntityManagerInterface $entityManager - ): array { - $isError = false; + protected function buildDeleteTree(array $entities, array $relations): array { + $isError = false; $propertyAccessor = PropertyAccess::createPropertyAccessor(); - $inflector = InflectorFactory::create()->build(); - $readableType = str_replace('_', ' ', Utils::tableForEntity($entities[0])); - $metadata = $entityManager->getClassMetadata($entities[0]::class); - $primaryKeyData = []; - $messages = []; + $inflector = InflectorFactory::create()->build(); + $readableType = str_replace('_', ' ', Utils::tableForEntity($entities[0])); + $metadata = $this->em->getClassMetadata($entities[0]::class); + $primaryKeyData = []; + $messages = []; foreach ($entities as $entity) { $primaryKeyDataTemp = []; foreach ($metadata->getIdentifierColumnNames() as $primaryKeyColumn) { $primaryKeyColumnValue = $propertyAccessor->getValue($entity, $primaryKeyColumn); - $primaryKeyDataTemp[] = $primaryKeyColumnValue; + $primaryKeyDataTemp[] = $primaryKeyColumnValue; // Check all relationships. foreach ($relations as $table => $tableRelations) { foreach ($tableRelations as $column => $constraint) { // If the target class and column match, check if there are any entities with this value. if ($constraint['targetColumn'] === $primaryKeyColumn && $constraint['target'] === $entity::class) { - $count = (int)$entityManager->createQueryBuilder() + $count = (int)$this->em->createQueryBuilder() ->from($table, 't') ->select(sprintf('COUNT(t.%s) AS cnt', $column)) ->andWhere(sprintf('t.%s = :value', $column)) @@ -309,8 +323,8 @@ protected function buildDeleteTree( ->getQuery() ->getSingleScalarResult(); if ($count > 0) { - $parts = explode('\\', $table); - $targetEntityType = $parts[count($parts) - 1]; + $parts = explode('\\', $table); + $targetEntityType = $parts[count($parts) - 1]; $targetReadableType = str_replace( '_', ' ', $inflector->tableize($inflector->pluralize($targetEntityType)) @@ -318,13 +332,13 @@ protected function buildDeleteTree( switch ($constraint['type']) { case 'CASCADE': - $message = sprintf('Cascade to %s', $targetReadableType); + $message = sprintf('Cascade to %s', $targetReadableType); $dependentEntities = $this->getDependentEntities($table, $relations); if (!empty($dependentEntities)) { $dependentEntitiesReadable = []; foreach ($dependentEntities as $dependentEntity) { - $parts = explode('\\', $dependentEntity); - $dependentEntityType = $parts[count($parts) - 1]; + $parts = explode('\\', $dependentEntity); + $dependentEntityType = $parts[count($parts) - 1]; $dependentEntitiesReadable[] = str_replace( '_', ' ', $inflector->tableize($inflector->pluralize($dependentEntityType)) @@ -341,11 +355,11 @@ protected function buildDeleteTree( $messages[] = sprintf('Create dangling references in %s', $targetReadableType); break; case null: - $isError = true; + $isError = true; $messages = [ sprintf('%s with %s "%s" is still referenced in %s, cannot delete.', - ucfirst($readableType), $primaryKeyColumn, $primaryKeyColumnValue, - $targetReadableType) + ucfirst($readableType), $primaryKeyColumn, $primaryKeyColumnValue, + $targetReadableType), ]; break 4; } @@ -363,31 +377,32 @@ protected function buildDeleteTree( * Perform delete operation for the given entities. * * @param Object[] $entities + * * @throws DBALException * @throws NoResultException * @throws NonUniqueResultException */ protected function deleteEntities( Request $request, - EntityManagerInterface $entityManager, - DOMJudgeService $DOMJudgeService, - EventLogService $eventLogService, - KernelInterface $kernel, array $entities, string $redirectUrl - ) : Response { + ): Response { // Assume that we only delete entities of the same class. foreach ($entities as $entity) { assert($entities[0]::class === $entity::class); } // Determine all the relationships between all tables using Doctrine cache. - $dir = realpath(sprintf('%s/src/Entity', $kernel->getProjectDir())); - $files = glob($dir . '/*.php'); - $relations = $this->getDatabaseRelations($files, $entityManager); + $dir = realpath(sprintf('%s/src/Entity', $this->kernel->getProjectDir())); + $files = glob($dir . '/*.php'); + $relations = $this->getDatabaseRelations($files); $readableType = str_replace('_', ' ', Utils::tableForEntity($entities[0])); - $messages = []; + $messages = []; - [$isError, $primaryKeyData, $deleteTreeMessages] = $this->buildDeleteTree($entities, $relations, $entityManager); + [ + $isError, + $primaryKeyData, + $deleteTreeMessages, + ] = $this->buildDeleteTree($entities, $relations); if (!empty($deleteTreeMessages)) { $messages = $deleteTreeMessages; } @@ -399,10 +414,10 @@ protected function deleteEntities( $msgList = []; foreach ($entities as $id => $entity) { - $this->commitDeleteEntity($entity, $DOMJudgeService, $entityManager, $primaryKeyData[$id], $eventLogService); + $this->commitDeleteEntity($entity, $primaryKeyData[$id]); $description = $entity->getShortDescription(); $msgList[] = sprintf('Successfully deleted %s %s "%s"', - $readableType, implode(', ', $primaryKeyData[$id]), $description); + $readableType, implode(', ', $primaryKeyData[$id]), $description); } $msg = implode("\n", $msgList); @@ -473,7 +488,7 @@ protected function getDependentEntities(string $entityClass, array $relations): * * @return Contest[] */ - protected function contestsForEntity(mixed $entity, DOMJudgeService $dj): array + protected function contestsForEntity(mixed $entity): array { // Determine contests to emit an event for the given entity: // * If the entity is a Problem entity, use the getContest() @@ -487,7 +502,7 @@ protected function contestsForEntity(mixed $entity, DOMJudgeService $dj): array // Otherwise, use the currently active contests. $contests = []; if ($entity instanceof Team || $entity instanceof TeamCategory) { - $possibleContests = $dj->getCurrentContests(); + $possibleContests = $this->dj->getCurrentContests(); foreach ($possibleContests as $contest) { if ($entity->inContest($contest)) { $contests[] = $contest; @@ -504,7 +519,7 @@ protected function contestsForEntity(mixed $entity, DOMJudgeService $dj): array } elseif (method_exists($entity, 'getContests')) { $contests = $entity->getContests(); } else { - $contests = $dj->getCurrentContests(); + $contests = $this->dj->getCurrentContests(); } return $contests; @@ -528,4 +543,37 @@ protected function streamResponse(RequestStack $requestStack, callable $callback }); return $response; } + + /** + * @param callable(): string $urlGenerator + * @param callable(): ?Response|null $saveCallback + */ + protected function processAddFormForExternalIdEntity( + FormInterface $form, + ExternalIdFromInternalIdInterface|CalculatedExternalIdBasedOnRelatedFieldInterface $entity, + callable $urlGenerator, + ?callable $saveCallback = null + ): ?Response { + if ($form->isSubmitted() && $form->isValid()) { + try { + if ($saveCallback) { + if ($response = $saveCallback()) { + return $response; + } + } else { + $this->saveEntity($entity, null, true); + } + return $this->redirect($urlGenerator()); + } catch (ExternalIdAlreadyExistsException $e) { + $message = sprintf( + 'The auto assigned external ID \'%s\' is already in use. Please type one yourself.', + $e->externalid + ); + $form->get('externalid')->addError(new FormError($message)); + return null; + } + } + + return null; + } } diff --git a/webapp/src/Controller/Jury/AnalysisController.php b/webapp/src/Controller/Jury/AnalysisController.php index 56506ba77f9..932f66757dd 100644 --- a/webapp/src/Controller/Jury/AnalysisController.php +++ b/webapp/src/Controller/Jury/AnalysisController.php @@ -10,6 +10,7 @@ use App\Service\StatisticsService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -99,6 +100,7 @@ public function teamAction(Team $team): Response #[Route(path: '/problem/{probid}', name: 'analysis_problem')] public function problemAction( + #[MapEntity(id: 'probid')] Problem $problem, #[MapQueryParameter] ?string $view = null @@ -118,4 +120,25 @@ public function problemAction( $this->stats->getProblemStats($contest, $problem, $view) ); } + + #[Route(path: '/languages', name: 'analysis_languages')] + public function languagesAction( + #[MapQueryParameter] + ?string $view = null + ): Response { + $contest = $this->dj->getCurrentContest(); + + if ($contest === null) { + return $this->render('jury/error.html.twig', [ + 'error' => 'No contest selected', + ]); + } + + $filterKeys = array_keys(StatisticsService::FILTERS); + $view = $view ?: reset($filterKeys); + + return $this->render('jury/analysis/languages.html.twig', + $this->stats->getLanguagesStats($contest, $view) + ); + } } diff --git a/webapp/src/Controller/Jury/AuditLogController.php b/webapp/src/Controller/Jury/AuditLogController.php index e43aa7f2b3d..a7adecf228c 100644 --- a/webapp/src/Controller/Jury/AuditLogController.php +++ b/webapp/src/Controller/Jury/AuditLogController.php @@ -156,7 +156,7 @@ private function generateDatatypeUrl(string $type, int|string|null $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 dd73acbbf00..6ec97828594 100644 --- a/webapp/src/Controller/Jury/ClarificationController.php +++ b/webapp/src/Controller/Jury/ClarificationController.php @@ -115,7 +115,6 @@ public function indexAction( 'oldClarifications' => $oldClarifications, 'generalClarifications' => $generalClarifications, 'queues' => $queues, - 'showExternalId' => $this->eventLogService->externalIdFieldForEntity(Clarification::class), 'currentQueue' => $currentQueue, 'currentFilter' => $currentFilter, 'categories' => $categories, @@ -140,7 +139,6 @@ public function viewAction(Request $request, int $id): Response } $parameters = ['list' => []]; - $parameters['showExternalId'] = $this->eventLogService->externalIdFieldForEntity(Clarification::class); $formData = [ 'recipient' => JuryClarificationType::RECIPIENT_MUST_SELECT, diff --git a/webapp/src/Controller/Jury/ConfigController.php b/webapp/src/Controller/Jury/ConfigController.php index b9c34dbb113..fca7890b512 100644 --- a/webapp/src/Controller/Jury/ConfigController.php +++ b/webapp/src/Controller/Jury/ConfigController.php @@ -65,13 +65,48 @@ public function indexAction(EventLogService $eventLogService, Request $request): } } } - $errors = $this->config->saveChanges($data, $eventLogService, $this->dj, $options); + $before = $this->config->all(); + $errors = $this->config->saveChanges($data, $eventLogService, $this->dj, options: $options); + $after = $this->config->all(); + + // Compile a list of differences. + $diffs = []; + foreach ($before as $key => $value) { + if (!array_key_exists($key, $after)) { + $diffs[$key] = ['before' => $value, 'after' => null]; + } elseif ($value !== $after[$key]) { + $diffs[$key] = ['before' => $value, 'after' => $after[$key]]; + } + } + foreach ($after as $key => $value) { + if (!array_key_exists($key, $before)) { + $diffs[$key] = ['before' => null, 'after' => $value]; + } + } if (empty($errors)) { - $this->addFlash('scoreboard_refresh', 'After changing specific ' . - 'settings, you might need to refresh the scoreboard.'); + $needsRefresh = false; + $needsRejudging = false; + foreach ($diffs as $key => $diff) { + $category = $this->config->getCategory($key); + if ($category === 'Scoring') { + $needsRefresh = true; + } + if ($category === 'Judging') { + $needsRejudging = true; + } + } + + if ($needsRefresh) { + $this->addFlash('scoreboard_refresh', 'After changing specific ' . + 'scoring related settings, you might need to refresh the scoreboard (cache).'); + } + if ($needsRejudging) { + $this->addFlash('danger', 'After changing specific ' . + 'judging related settings, you might need to rejudge affected submissions.'); + } - return $this->redirectToRoute('jury_config'); + return $this->redirectToRoute('jury_config', ['diffs' => json_encode($diffs)]); } else { $this->addFlash('danger', 'Some errors occurred while saving configuration, ' . 'please check the data you entered.'); @@ -114,10 +149,15 @@ public function indexAction(EventLogService $eventLogService, Request $request): 'data' => $data ]; } + $diffs = $request->query->get('diffs'); + if ($diffs !== null) { + $diffs = json_decode($diffs, true); + } return $this->render('jury/config.html.twig', [ 'options' => $allData, 'errors' => $errors ?? [], 'activeCategory' => $activeCategory ?? 'Scoring', + 'diffs' => $diffs, ]); } diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index ad41392bbd8..949c1c4fefb 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -37,12 +37,14 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted('ROLE_JURY')] @@ -52,13 +54,15 @@ class ContestController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly AssetUpdateService $assetUpdater - ) {} + protected readonly AssetUpdateService $assetUpdater, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NonUniqueResultException @@ -69,106 +73,7 @@ public function indexAction(Request $request): Response { $em = $this->em; - if ($doNow = $request->request->all('donow')) { - $times = ['activate', 'start', 'freeze', 'end', - 'unfreeze', 'finalize', 'deactivate']; - $start_actions = ['delay_start', 'resume_start']; - $actions = array_merge($times, $start_actions); - - if (!$this->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedHttpException(); - } - $contest = $em->getRepository(Contest::class)->find($request->request->get('contest')); - if (!$contest) { - throw new NotFoundHttpException('Contest not found'); - } - - $time = key($doNow); - if (!in_array($time, $actions, true)) { - throw new BadRequestHttpException( - sprintf("Unknown value '%s' for timetype", $time) - ); - } - - if ($time === 'finalize') { - return $this->redirectToRoute( - 'jury_contest_finalize', - ['contestId' => $contest->getCid()] - ); - } - - $now = (int)floor(Utils::now()); - $nowstring = date('Y-m-d H:i:s ', $now) . date_default_timezone_get(); - $this->dj->auditlog('contest', $contest->getCid(), $time . ' now', $nowstring); - - // Special case delay/resume start (only sets/unsets starttime_undefined). - $maxSeconds = Contest::STARTTIME_UPDATE_MIN_SECONDS_BEFORE; - if (in_array($time, $start_actions, true)) { - $enabled = $time !== 'delay_start'; - if (Utils::difftime((float)$contest->getStarttime(false), $now) <= $maxSeconds) { - $this->addFlash( - 'error', - sprintf("Cannot %s less than %d seconds before contest start.", - $time, $maxSeconds) - ); - return $this->redirectToRoute('jury_contests'); - } - $contest->setStarttimeEnabled($enabled); - $em->flush(); - $this->eventLogService->log( - 'contest', - $contest->getCid(), - EventLogService::ACTION_UPDATE, - $contest->getCid() - ); - $this->addFlash('scoreboard_refresh', - 'After changing the contest start time, it may be ' . - 'necessary to recalculate any cached scoreboards.'); - return $this->redirectToRoute('jury_contests'); - } - - $juryTimeData = $contest->getDataForJuryInterface(); - if (!$juryTimeData[$time]['show_button']) { - throw new BadRequestHttpException( - sprintf('Cannot update %s time at this moment', $time) - ); - } - - // starttime is special because other, relative times depend on it. - if ($time == 'start') { - if ($contest->getStarttimeEnabled() && - Utils::difftime((float)$contest->getStarttime(false), - $now) <= $maxSeconds) { - $this->addFlash( - 'danger', - sprintf("Cannot update starttime less than %d seconds before contest start.", - $maxSeconds) - ); - return $this->redirectToRoute('jury_contests'); - } - $contest - ->setStarttime($now) - ->setStarttimeString($nowstring) - ->setStarttimeEnabled(true); - $em->flush(); - - $this->addFlash('scoreboard_refresh', - 'After changing the contest start time, it may be ' . - 'necessary to recalculate any cached scoreboards.'); - } else { - $method = sprintf('set%stimeString', $time); - $contest->{$method}($nowstring); - $em->flush(); - } - $this->eventLogService->log( - 'contest', - $contest->getCid(), - EventLogService::ACTION_UPDATE, - $contest->getCid() - ); - return $this->redirectToRoute('jury_contests'); - } - + /** @var Contest[] $contests */ $contests = $em->createQueryBuilder() ->select('c') ->from(Contest::class, 'c') @@ -178,6 +83,7 @@ public function indexAction(Request $request): Response $table_fields = [ 'cid' => ['title' => 'CID', 'sort' => true], + 'externalid' => ['title' => "external ID", 'sort' => true], 'shortname' => ['title' => 'shortname', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], 'activatetime' => ['title' => 'activate', 'sort' => true], @@ -225,13 +131,6 @@ public function indexAction(Request $request): Response 'num_problems' => ['title' => '# problems', 'sort' => true], ]); - // Insert external ID field when configured to use it - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Contest::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => "external ID", 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $contests_table = []; foreach ($contests as $contest) { @@ -244,31 +143,69 @@ public function indexAction(Request $request): Response } } - if ($this->isGranted('ROLE_ADMIN') && !$contest->isLocked()) { + // Create action links + if ($contest->getContestProblemsetType()) { $contestactions[] = [ - 'icon' => 'edit', - 'title' => 'edit this contest', - 'link' => $this->generateUrl('jury_contest_edit', [ - 'contestId' => $contest->getCid(), + 'icon' => 'file-' . $contest->getContestProblemsetType(), + 'title' => 'view contest problemset document', + 'link' => $this->generateUrl('jury_contest_problemset', [ + 'cid' => $contest->getCid(), ]) ]; - $contestactions[] = [ - 'icon' => 'trash-alt', - 'title' => 'delete this contest', - 'link' => $this->generateUrl('jury_contest_delete', [ - 'contestId' => $contest->getCid(), - ]), - 'ajaxModal' => true, - ]; + } else { + $contestactions[] = []; + } + if ($this->isGranted('ROLE_ADMIN')) { + if ($contest->isLocked()) { + // The number of table columns and thus the number of actions need + // to match for all rows to not get DataTables errors. + // Since we add two actions for non-locked contests, we need to add + // two empty actions for locked contests. + $contestactions[] = []; + $contestactions[] = []; + } else { + $contestactions[] = [ + 'icon' => 'edit', + 'title' => 'edit this contest', + 'link' => $this->generateUrl('jury_contest_edit', [ + 'contestId' => $contest->getCid(), + ]) + ]; + $contestactions[] = [ + 'icon' => 'trash-alt', + 'title' => 'delete this contest', + 'link' => $this->generateUrl('jury_contest_delete', [ + 'contestId' => $contest->getCid(), + ]), + 'ajaxModal' => true, + ]; + } } $contestdata['process_balloons'] = [ - 'value' => $contest->getProcessBalloons() ? 'yes' : 'no' + 'toggle_partial' => 'contest_toggle.html.twig', + 'partial_arguments' => [ + 'type' => 'balloons', + 'contest' => $contest, + 'enabled' => $contest->getProcessBalloons(), + ], ]; $contestdata['medals_enabled'] = [ - 'value' => $contest->getMedalsEnabled() ? 'yes' : 'no' + 'toggle_partial' => 'contest_toggle.html.twig', + 'partial_arguments' => [ + 'type' => 'medals', + 'contest' => $contest, + 'enabled' => $contest->getMedalsEnabled(), + ], + ]; + $contestdata['public'] = [ + 'toggle_partial' => 'contest_toggle.html.twig', + 'partial_arguments' => [ + 'type' => 'public', + 'contest' => $contest, + 'enabled' => $contest->getPublic(), + ], ]; - $contestdata['public'] = ['value' => $contest->getPublic() ? 'yes' : 'no']; if ($contest->isOpenToAllTeams()) { $contestdata['num_teams'] = ['value' => 'all']; } else { @@ -419,20 +356,52 @@ public function viewAction(Request $request, int $contestId): Response ]); } - #[Route(path: '/{contestId}/toggle-submit', name: 'jury_contest_toggle_submit')] - public function toggleSubmitAction(Request $request, string $contestId): Response - { + #[Route(path: '/{contestId}/toggle/{type}', name: 'jury_contest_toggle')] + public function toggleSubmitAction( + RouterInterface $router, + Request $request, + string $contestId, + string $type + ): Response { $contest = $this->em->getRepository(Contest::class)->find($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } - $contest->setAllowSubmit($request->request->getBoolean('allow_submit')); + $value = $request->request->getBoolean('value'); + + switch ($type) { + case 'submit': + $contest->setAllowSubmit($value); + $label = 'set allow submit'; + break; + case 'balloons': + $contest->setProcessBalloons($value); + $label = 'set process balloons'; + break; + case 'tiebreaker': + $contest->setRuntimeAsScoreTiebreaker($value); + $label = 'set runtime as tiebreaker'; + break; + case 'medals': + $contest->setMedalsEnabled($value); + $label = 'set medal processing'; + break; + case 'public': + $contest->setPublic($value); + $label = 'set publicly visible'; + break; + default: + throw new BadRequestHttpException('Unknown toggle type'); + } $this->em->flush(); - $this->dj->auditlog('contest', $contestId, 'set allow submit', - $request->request->getBoolean('allow_submit') ? 'yes' : 'no'); - return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); + $this->dj->auditlog('contest', $contestId, $label, $value ? 'yes' : 'no'); + return $this->redirectToLocalReferrer( + $router, + $request, + $this->generateUrl('jury_contest', ['contestId' => $contestId]) + ); } #[Route(path: '/{contestId<\d+>}/remove-interval/{intervalId}', name: 'jury_contest_remove_interval', methods: ['POST'])] @@ -577,24 +546,19 @@ public function editAction(Request $request, int $contestId): Response $deletedProblems = $getDeletedEntities($contest->getProblems(), 'getProbid'); $this->assetUpdater->updateAssets($contest); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $contest, - $contest->getCid(), false); - - $teamEndpoint = $this->eventLogService->endpointForEntity(Team::class); - $teamCategoryEndpoint = $this->eventLogService->endpointForEntity(TeamCategory::class); - $problemEndpoint = $this->eventLogService->endpointForEntity(Problem::class); + $this->saveEntity($contest, $contest->getCid(), false); // TODO: cascade deletes. Maybe use getDependentEntities()? foreach ($deletedTeams as $team) { - $this->eventLogService->log($teamEndpoint, $team->getTeamid(), + $this->eventLogService->log('teams', $team->getTeamid(), EventLogService::ACTION_DELETE, $contest->getCid(), null, null, false); } foreach ($deletedTeamCategories as $category) { - $this->eventLogService->log($teamCategoryEndpoint, $category->getCategoryid(), + $this->eventLogService->log('groups', $category->getCategoryid(), EventLogService::ACTION_DELETE, $contest->getCid(), null, null, false); } foreach ($deletedProblems as $problem) { - $this->eventLogService->log($problemEndpoint, $problem->getProbid(), + $this->eventLogService->log('problems', $problem->getProbid(), EventLogService::ACTION_DELETE, $contest->getCid(), null, null, false); } return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getcid()]); @@ -622,8 +586,7 @@ public function deleteAction(Request $request, int $contestId): Response return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$contest], $this->generateUrl('jury_contests')); + return $this->deleteEntities($request, [$contest], $this->generateUrl('jury_contests')); } #[IsGranted('ROLE_ADMIN')] @@ -646,8 +609,7 @@ public function deleteProblemAction(Request $request, int $contestId, int $probI return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$contestProblem], $this->generateUrl('jury_contest', ['contestId' => $contestId])); + return $this->deleteEntities($request, [$contestProblem], $this->generateUrl('jury_contest', ['contestId' => $contestId])); } #[IsGranted('ROLE_ADMIN')] @@ -662,38 +624,45 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $response = $this->checkTimezones($form); - if ($response !== null) { - return $response; - } - - $this->em->wrapInTransaction(function () use ($contest) { - // A little 'hack': we need to first persist and save the - // contest, before we can persist and save the problem, - // because we need a contest ID. - /** @var ContestProblem[] $problems */ - $problems = $contest->getProblems()->toArray(); - foreach ($contest->getProblems() as $problem) { - $contest->removeProblem($problem); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $contest, + fn () => $this->generateUrl('jury_contest', ['contestId' => $contest->getcid()]), + function () use ($form, $contest) { + $response = $this->checkTimezones($form); + if ($response !== null) { + return $response; } - $this->em->persist($contest); - $this->em->flush(); - // Now we can assign the problems to the contest and persist them. - foreach ($problems as $problem) { - $problem->setContest($contest); - $this->em->persist($problem); - } - $this->assetUpdater->updateAssets($contest); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $contest, null, true); - // Note that we do not send out create events for problems, - // teams and team categories for this contest. This happens - // when someone connects to the event feed (or we have a - // dependent event) anyway and adding the code here would - // overcomplicate this function. - }); - return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getcid()]); + $this->em->wrapInTransaction(function () use ($contest) { + // A little 'hack': we need to first persist and save the + // contest, before we can persist and save the problem, + // because we need a contest ID. + /** @var ContestProblem[] $problems */ + $problems = $contest->getProblems()->toArray(); + foreach ($contest->getProblems() as $problem) { + $contest->removeProblem($problem); + } + $this->em->persist($contest); + $this->em->flush(); + + // Now we can assign the problems to the contest and persist them. + foreach ($problems as $problem) { + $problem->setContest($contest); + $this->em->persist($problem); + } + $this->assetUpdater->updateAssets($contest); + $this->saveEntity($contest, null, true); + // Note that we do not send out create events for problems, + // teams and team categories for this contest. This happens + // when someone connects to the event feed (or we have a + // dependent event) anyway and adding the code here would + // overcomplicate this function. + }); + + return null; + } + )) { + return $response; } return $this->render('jury/contest_add.html.twig', [ @@ -849,6 +818,102 @@ public function finalizeAction(Request $request, int $contestId): Response ]); } + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/{contestId<\d+>}/{time}/doNow', name: 'jury_contest_donow')] + public function doNowAction(Request $request, int $contestId, string $time): Response + { + $times = ['activate', 'start', 'freeze', 'end', 'unfreeze', 'finalize', 'deactivate']; + $start_actions = ['delay_start', 'resume_start']; + $actions = array_merge($times, $start_actions); + + $contest = $this->em->getRepository(Contest::class)->find($contestId); + if (!$contest) { + throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); + } + + if (!in_array($time, $actions, true)) { + throw new BadRequestHttpException(sprintf("Unknown value '%s' for timetype", $time)); + } + + if ($time === 'finalize') { + return $this->redirectToRoute('jury_contest_finalize', ['contestId' => $contest->getCid()]); + } + + $now = (int)floor(Utils::now()); + $nowstring = date('Y-m-d H:i:s ', $now) . date_default_timezone_get(); + $this->dj->auditlog('contest', $contest->getCid(), $time . ' now', $nowstring); + + // Special case delay/resume start (only sets/unsets starttime_undefined). + $maxSeconds = Contest::STARTTIME_UPDATE_MIN_SECONDS_BEFORE; + if (in_array($time, $start_actions, true)) { + $enabled = $time !== 'delay_start'; + if (Utils::difftime((float)$contest->getStarttime(false), $now) <= $maxSeconds) { + $this->addFlash( + 'error', + sprintf("Cannot '%s' less than %d seconds before contest start.", + $time, $maxSeconds) + ); + return $this->redirectToRoute('jury_contests'); + } + $contest->setStarttimeEnabled($enabled); + $this->em->flush(); + $this->eventLogService->log( + 'contest', + $contest->getCid(), + EventLogService::ACTION_UPDATE, + $contest->getCid() + ); + $this->addFlash('scoreboard_refresh', 'After changing the contest start time, it may be ' + . 'necessary to recalculate any cached scoreboards.'); + return $this->redirectToRoute('jury_contests'); + } + + $juryTimeData = $contest->getDataForJuryInterface(); + if (!$juryTimeData[$time]['show_button']) { + throw new BadRequestHttpException( + sprintf("Cannot update '%s' time at this moment", $time) + ); + } + + // starttime is special because other, relative times depend on it. + if ($time == 'start') { + if ($contest->getStarttimeEnabled() && + Utils::difftime((float)$contest->getStarttime(false), + $now) <= $maxSeconds) { + $this->addFlash( + 'danger', + sprintf("Cannot update starttime less than %d seconds before contest start.", + $maxSeconds) + ); + return $this->redirectToRoute('jury_contests'); + } + $contest + ->setStarttime($now) + ->setStarttimeString($nowstring) + ->setStarttimeEnabled(true); + $this->em->flush(); + + $this->addFlash('scoreboard_refresh', 'After changing the contest start time, it may be ' + . 'necessary to recalculate any cached scoreboards.'); + } else { + $method = sprintf('set%stimeString', $time); + $contest->{$method}($nowstring); + $this->em->flush(); + } + $this->eventLogService->log( + 'contest', + $contest->getCid(), + EventLogService::ACTION_UPDATE, + $contest->getCid() + ); + + $referer = $request->headers->get('referer'); + if ($referer) { + return $this->redirect($referer); + } + return $this->redirectToRoute('jury_contests'); + } + #[Route(path: '/{contestId<\d+>}/request-remaining', name: 'jury_contest_request_remaining')] public function requestRemainingRunsWholeContestAction(int $contestId): RedirectResponse { @@ -987,4 +1052,15 @@ public function publicScoreboardDataZipAction( } return $this->dj->getScoreboardZip($request, $requestStack, $contest, $scoreboardService, $type === 'unfrozen'); } + + #[Route(path: '/{cid<\d+>}/problemset', name: 'jury_contest_problemset')] + public function viewProblemsetAction(int $cid): StreamedResponse + { + $contest = $this->em->getRepository(Contest::class)->find($cid); + if (!$contest) { + throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $cid)); + } + + return $contest->getContestProblemsetStreamedResponse(); + } } diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index 63954b325bf..b49ef5e184e 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -34,12 +34,14 @@ class ExecutableController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, - protected readonly EventLogService $eventLogService - ) {} + KernelInterface $kernel, + protected readonly EventLogService $eventLogService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_executables')] public function indexAction(Request $request): Response @@ -357,8 +359,7 @@ public function viewAction( $executable->setImmutableExecutable( $this->dj->createImmutableExecutable($zip) ); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $executable, - $executable->getExecid(), false); + $this->saveEntity($executable, $executable->getExecid(), false); return $this->redirectToRoute('jury_executable', ['execId' => $executable->getExecid()]); } @@ -482,8 +483,7 @@ public function deleteAction(Request $request, string $execId): Response throw new NotFoundHttpException(sprintf('Executable with ID %s not found', $execId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$executable], $this->generateUrl('jury_executables')); + return $this->deleteEntities($request, [$executable], $this->generateUrl('jury_executables')); } /** diff --git a/webapp/src/Controller/Jury/ExternalContestController.php b/webapp/src/Controller/Jury/ExternalContestController.php index 971a54919df..d96759cc545 100644 --- a/webapp/src/Controller/Jury/ExternalContestController.php +++ b/webapp/src/Controller/Jury/ExternalContestController.php @@ -25,13 +25,15 @@ class ExternalContestController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLog, - protected readonly KernelInterface $kernel, - private readonly ExternalContestSourceService $sourceService - ) {} + EventLogService $eventLog, + KernelInterface $kernel, + private readonly ExternalContestSourceService $sourceService, + ) { + parent::__construct($em, $eventLog, $dj, $kernel); + } #[Route(path: '/', name: 'jury_external_contest')] public function indexAction(Request $request): Response @@ -156,7 +158,7 @@ public function manageAction(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $this->em->persist($externalContestSource); - $this->saveEntity($this->em, $this->eventLog, $this->dj, $externalContestSource, null, true); + $this->saveEntity($externalContestSource, null, true); return $this->redirectToRoute('jury_external_contest'); } diff --git a/webapp/src/Controller/Jury/ImportExportController.php b/webapp/src/Controller/Jury/ImportExportController.php index 922d5de1a89..f8c28b07a10 100644 --- a/webapp/src/Controller/Jury/ImportExportController.php +++ b/webapp/src/Controller/Jury/ImportExportController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\ResultRow; use App\Entity\Clarification; use App\Entity\Contest; use App\Entity\ContestProblem; @@ -10,6 +11,7 @@ use App\Entity\TeamCategory; use App\Form\Type\ContestExportType; use App\Form\Type\ContestImportType; +use App\Form\Type\ExportResultsType; use App\Form\Type\ICPCCmsType; use App\Form\Type\JsonImportType; use App\Form\Type\ProblemsImportType; @@ -30,6 +32,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\SubmitButton; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; @@ -44,23 +47,28 @@ use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Twig\Environment; -#[IsGranted('ROLE_ADMIN')] #[Route(path: '/jury/import-export')] +#[IsGranted('ROLE_JURY')] class ImportExportController extends BaseController { public function __construct( protected readonly ICPCCmsService $icpcCmsService, protected readonly ImportExportService $importExportService, - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly ScoreboardService $scoreboardService, - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly EventLogService $eventLogService, protected readonly ImportProblemService $importProblemService, + KernelInterface $kernel, #[Autowire('%domjudge.version%')] - protected readonly string $domjudgeVersion - ) {} + protected readonly string $domjudgeVersion, + protected readonly Environment $twig, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws ClientExceptionInterface @@ -70,6 +78,7 @@ public function __construct( * @throws TransportExceptionInterface */ #[Route(path: '', name: 'jury_import_export')] + #[IsGranted('ROLE_ADMIN')] public function indexAction(Request $request): Response { $tsvForm = $this->createForm(TsvImportType::class); @@ -238,30 +247,86 @@ public function indexAction(Request $request): Response $this->addFlash('danger', "Parse error in YAML/JSON file (" . $file->getClientOriginalName() . "): " . $e->getMessage()); return $this->redirectToRoute('jury_import_export'); } - if ($this->importExportService->importProblemsData($problemsImportForm->get('contest')->getData(), $data)) { + if ($this->importExportService->importProblemsData($problemsImportForm->get('contest')->getData(), $data, $ids, $messages)) { $this->addFlash('success', sprintf('The file %s is successfully imported.', $file->getClientOriginalName())); } else { - $this->addFlash('danger', 'Failed importing problems'); + if (!empty($messages)) { + $this->postMessages($messages); + } else { + $this->addFlash('danger', 'Failed importing problems'); + } } return $this->redirectToRoute('jury_import_export'); } - /** @var TeamCategory[] $teamCategories */ - $teamCategories = $this->em->createQueryBuilder() - ->from(TeamCategory::class, 'c', 'c.categoryid') - ->select('c.sortorder, c.name') - ->where('c.visible = 1') - ->orderBy('c.sortorder') - ->getQuery() - ->getResult(); - $sortOrders = []; - foreach ($teamCategories as $teamCategory) { - $sortOrder = $teamCategory['sortorder']; - if (!array_key_exists($sortOrder, $sortOrders)) { - $sortOrders[$sortOrder] = []; + $exportResultsForm = $this->createForm(ExportResultsType::class); + + $exportResultsForm->handleRequest($request); + + if ($exportResultsForm->isSubmitted() && $exportResultsForm->isValid()) { + $contest = $this->dj->getCurrentContest(); + if ($contest === null) { + throw new BadRequestHttpException('No current contest'); } - $sortOrders[$sortOrder][] = $teamCategory['name']; + + $data = $exportResultsForm->getData(); + $format = $data['format']; + $sortOrder = $data['sortorder']; + $individuallyRanked = $data['individually_ranked']; + $honors = $data['honors']; + + $extension = match ($format) { + 'html_inline', 'html_download' => 'html', + 'tsv' => 'tsv', + default => throw new BadRequestHttpException('Invalid format'), + }; + $contentType = match ($format) { + 'html_inline' => 'text/html', + 'html_download' => 'text/html', + 'tsv' => 'text/csv', + default => throw new BadRequestHttpException('Invalid format'), + }; + $contentDisposition = match ($format) { + 'html_inline' => 'inline', + 'html_download', 'tsv' => 'attachment', + default => throw new BadRequestHttpException('Invalid format'), + }; + $filename = 'results.' . $extension; + + $response = new StreamedResponse(); + $response->setCallback(function () use ( + $format, + $sortOrder, + $individuallyRanked, + $honors + ) { + if ($format === 'tsv') { + $data = $this->importExportService->getResultsData( + $sortOrder->sort_order, + $individuallyRanked, + $honors, + ); + + echo "results\t1\n"; + foreach ($data as $row) { + echo implode("\t", array_map(fn($field) => Utils::toTsvField((string)$field), $row->toArray())) . "\n"; + } + } else { + echo $this->getResultsHtml( + $sortOrder->sort_order, + $individuallyRanked, + $honors, + ); + } + }); + $response->headers->set('Content-Type', $contentType); + $response->headers->set('Content-Disposition', "$contentDisposition; filename=\"$filename\""); + $response->headers->set('Content-Transfer-Encoding', 'binary'); + $response->headers->set('Connection', 'Keep-Alive'); + $response->headers->set('Accept-Ranges', 'bytes'); + + return $response; } return $this->render('jury/import_export.html.twig', [ @@ -272,16 +337,13 @@ public function indexAction(Request $request): Response 'contest_export_form' => $contestExportForm, 'contest_import_form' => $contestImportForm, 'problems_import_form' => $problemsImportForm, - 'sort_orders' => $sortOrders, + 'export_results_form' => $exportResultsForm, ]); } #[Route(path: '/export/{type}.tsv', name: 'jury_tsv_export')] - public function exportTsvAction( - string $type, - #[MapQueryParameter(name: 'sort_order')] - ?int $sortOrder, - ): Response { + public function exportTsvAction(string $type): Response + { $data = []; $tsvType = $type; try { @@ -292,14 +354,6 @@ public function exportTsvAction( case 'teams': $data = $this->importExportService->getTeamData(); break; - case 'wf_results': - $data = $this->importExportService->getResultsData($sortOrder); - $tsvType = 'results'; - break; - case 'full_results': - $data = $this->importExportService->getResultsData($sortOrder, full: true); - $tsvType = 'results'; - break; } } catch (BadRequestHttpException $e) { $this->addFlash('danger', $e->getMessage()); @@ -322,29 +376,22 @@ public function exportTsvAction( return $response; } - #[Route(path: '/export/{type}.html', name: 'jury_html_export')] - public function exportHtmlAction(Request $request, string $type): Response + #[Route(path: '/export/clarifications.html', name: 'jury_html_export_clarifications')] + public function exportClarificationsHtmlAction(): Response { try { - switch ($type) { - case 'wf_results': - return $this->getResultsHtml($request); - case 'full_results': - return $this->getResultsHtml($request, full: true); - case 'clarifications': - return $this->getClarificationsHtml(); - default: - $this->addFlash('danger', "Unknown export type '" . $type . "' requested."); - return $this->redirectToRoute('jury_import_export'); - } + return $this->getClarificationsHtml(); } catch (BadRequestHttpException $e) { $this->addFlash('danger', $e->getMessage()); return $this->redirectToRoute('jury_import_export'); } } - protected function getResultsHtml(Request $request, bool $full = false): Response - { + protected function getResultsHtml( + int $sortOrder, + bool $individuallyRanked, + bool $honors + ): string { /** @var TeamCategory[] $categories */ $categories = $this->em->createQueryBuilder() ->from(TeamCategory::class, 'c', 'c.categoryid') @@ -377,36 +424,37 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons $ranked = []; $honorable = []; $regionWinners = []; + $rankPerTeam = []; - $sortOrder = $request->query->getInt('sort_order'); - - foreach ($this->importExportService->getResultsData($sortOrder, full: $full) as $row) { - $team = $teamNames[$row[0]]; + foreach ($this->importExportService->getResultsData($sortOrder, $individuallyRanked, $honors) as $row) { + $team = $teamNames[$row->teamId]; + $rankPerTeam[$row->teamId] = $row->rank; - if ($row[6] !== '') { + if ($row->groupWinner) { $regionWinners[] = [ - 'group' => $row[6], + 'group' => $row->groupWinner, 'team' => $team, + 'rank' => $row->rank ?? '-', ]; } $row = [ 'team' => $team, - 'rank' => $row[1], - 'award' => $row[2], - 'solved' => $row[3], - 'total_time' => $row[4], - 'max_time' => $row[5], + 'rank' => $row->rank, + 'award' => $row->award, + 'solved' => $row->numSolved, + 'total_time' => $row->totalTime, + 'max_time' => $row->timeOfLastSubmission, ]; if (preg_match('/^(.*) Medal$/', $row['award'], $matches)) { $row['class'] = strtolower($matches[1]); } else { $row['class'] = ''; } - if ($row['rank'] === '') { + if ($row['rank'] === null) { $honorable[] = $row['team']; - } elseif ($row['award'] === 'Ranked') { - $ranked[] = $row; + } elseif (in_array($row['award'], ['Ranked', 'Highest Honors', 'High Honors', 'Honors'], true)) { + $ranked[$row['award']][] = $row; } else { $awarded[] = $row; } @@ -416,13 +464,16 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons $collator = new Collator('en_US'); $collator->sort($honorable); - usort($ranked, function (array $a, array $b) use ($collator): int { - if ($a['rank'] !== $b['rank']) { - return $a['rank'] <=> $b['rank']; - } + foreach ($ranked as &$rankedTeams) { + usort($rankedTeams, function (array $a, array $b) use ($collator): int { + if ($a['rank'] !== $b['rank']) { + return $a['rank'] <=> $b['rank']; + } - return $collator->compare($a['team'], $b['team']); - }); + return $collator->compare($a['team'], $b['team']); + }); + } + unset($rankedTeams); $problems = $scoreboard->getProblems(); $matrix = $scoreboard->getMatrix(); @@ -434,6 +485,7 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons 'problem_name' => $problem->getProblem()->getName(), 'team' => null, 'time' => null, + 'rank' => null, ]; foreach ($teams as $team) { if (!isset($categories[$team->getCategory()->getCategoryid()]) || $team->getCategory()->getSortorder() !== $sortOrder) { @@ -446,6 +498,7 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons 'problem' => $problem->getShortname(), 'problem_name' => $problem->getProblem()->getName(), 'team' => $teamNames[$team->getIcpcId()], + 'rank' => $rankPerTeam[$team->getIcpcId()] ?: '-', 'time' => Utils::scoretime($matrixItem->time, $scoreIsInSeconds), ]; } @@ -473,16 +526,10 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons 'firstToSolve' => $firstToSolve, 'domjudgeVersion' => $this->domjudgeVersion, 'title' => sprintf('Results for %s', $contest->getName()), - 'download' => $request->query->getBoolean('download'), 'sortOrder' => $sortOrder, ]; - $response = $this->render('jury/export/results.html.twig', $data); - - if ($request->query->getBoolean('download')) { - $response->headers->set('Content-disposition', 'attachment; filename=results.html'); - } - return $response; + return $this->twig->render('jury/export/results.html.twig', $data); } protected function getClarificationsHtml(): Response diff --git a/webapp/src/Controller/Jury/InternalErrorController.php b/webapp/src/Controller/Jury/InternalErrorController.php index 6db1b2aa593..7f78c8ecaa1 100644 --- a/webapp/src/Controller/Jury/InternalErrorController.php +++ b/webapp/src/Controller/Jury/InternalErrorController.php @@ -9,10 +9,12 @@ use App\Entity\JudgeTask; use App\Entity\Problem; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\RejudgingService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -26,11 +28,15 @@ class InternalErrorController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly RejudgingService $rejudgingService, protected readonly RequestStack $requestStack, - ) {} + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_internal_errors')] public function indexAction(): Response @@ -124,10 +130,6 @@ public function viewAction(int $errorId): Response 'internalError' => $internalError, 'affectedLink' => $affectedLink, 'affectedText' => $affectedText, - 'refresh' => [ - 'after' => 15, - 'url' => $this->generateUrl('jury_internal_error', ['errorId' => $internalError->getErrorid()]), - ] ]); } diff --git a/webapp/src/Controller/Jury/JudgehostController.php b/webapp/src/Controller/Jury/JudgehostController.php index 1e0e2dcd7d1..eb23b44ee39 100644 --- a/webapp/src/Controller/Jury/JudgehostController.php +++ b/webapp/src/Controller/Jury/JudgehostController.php @@ -33,12 +33,14 @@ class JudgehostController extends BaseController // Note: when adding or modifying routes, make sure they do not clash with the /judgehosts/{hostname} route. public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLog, - protected readonly KernelInterface $kernel - ) {} + EventLogService $eventLog, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLog, $dj, $kernel); + } #[Route(path: '', name: 'jury_judgehosts')] public function indexAction(Request $request): Response @@ -269,8 +271,7 @@ public function deleteAction(Request $request, int $judgehostid): Response ->getQuery() ->getOneOrNullResult(); - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLog, $this->kernel, - [$judgehost], $this->generateUrl('jury_judgehosts')); + return $this->deleteEntities($request, [$judgehost], $this->generateUrl('jury_judgehosts')); } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index d24863f5731..77ab13c849d 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -13,12 +13,14 @@ use App\Entity\TeamAffiliation; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\ScoreboardService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -34,10 +36,14 @@ class JuryMiscController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, + protected readonly EventLogService $eventLogService, protected readonly RequestStack $requestStack, - ) {} + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_BALLOON') or is_granted('ROLE_CLARIFICATION_RW')"))] #[Route(path: '', name: 'jury_index')] diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index 2f36c011b66..734bfc1f139 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -22,6 +22,7 @@ use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted('ROLE_JURY')] @@ -31,12 +32,14 @@ class LanguageController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, - protected readonly EventLogService $eventLogService - ) {} + KernelInterface $kernel, + protected readonly EventLogService $eventLogService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_languages')] public function indexAction(): Response @@ -50,6 +53,7 @@ public function indexAction(): Response ->getQuery()->getResult(); $table_fields = [ 'langid' => ['title' => 'ID', 'sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true, 'default_sort' => true], 'entrypoint' => ['title' => 'entry point', 'sort' => true], 'allowjudge' => ['title' => 'allow judge', 'sort' => true], @@ -58,13 +62,6 @@ public function indexAction(): Response 'executable' => ['title' => 'executable', 'sort' => true], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Language::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $enabled_languages = []; $disabled_languages = []; @@ -98,12 +95,24 @@ public function indexAction(): Response $executable = $lang->getCompileExecutable(); + $allowJudgeOptions = [ + 'toggle_partial' => 'language_toggle.html.twig', + 'partial_arguments' => [ + 'path' => 'jury_language_toggle_judge', + 'language' => $lang, + 'value' => $lang->getAllowJudge(), + ], + ]; + + if (!$lang->getAllowJudge()) { + $allowJudgeOptions['cssclass'] = 'text-danger font-weight-bold'; + } + // Merge in the rest of the data. $langdata = array_merge($langdata, [ 'entrypoint' => ['value' => $lang->getRequireEntryPoint() ? 'yes' : 'no'], 'extensions' => ['value' => implode(', ', $lang->getExtensions())], - 'allowjudge' => $lang->getAllowJudge() ? - ['value' => 'yes'] : ['value' => 'no', 'cssclass'=>'text-danger font-weight-bold'], + 'allowjudge' => $allowJudgeOptions, 'executable' => [ 'value' => $executable === null ? '-' : $executable->getShortDescription(), 'link' => $executable === null ? null : $this->generateUrl('jury_executable', [ @@ -148,15 +157,22 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - // Normalize extensions - if ($language->getExtensions()) { - $language->setExtensions(array_values($language->getExtensions())); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $language, + fn() => $this->generateUrl('jury_language', ['langId' => $language->getLangid()]), + function () use ($language) { + // Normalize extensions + if ($language->getExtensions()) { + $language->setExtensions(array_values($language->getExtensions())); + } + $this->em->persist($language); + $this->saveEntity($language, + $language->getLangid(), true); + + return null; } - $this->em->persist($language); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $language, - $language->getLangid(), true); - return $this->redirectToRoute('jury_language', ['langId' => $language->getLangid()]); + )) { + return $response; } return $this->render('jury/language_add.html.twig', [ @@ -187,8 +203,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'refresh' => [ 'after' => 15, 'url' => $this->generateUrl('jury_language', ['langId' => $language->getLangid()]), @@ -213,23 +228,26 @@ public function toggleSubmitAction(Request $request, string $langId): Response throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } - $language->setAllowSubmit($request->request->getBoolean('allow_submit')); + $language->setAllowSubmit($request->request->getBoolean('value')); $this->em->flush(); $this->dj->auditlog('language', $langId, 'set allow submit', - $request->request->getBoolean('allow_submit') ? 'yes' : 'no'); + $request->request->getBoolean('value') ? 'yes' : 'no'); return $this->redirectToRoute('jury_language', ['langId' => $langId]); } #[Route(path: '/{langId}/toggle-judge', name: 'jury_language_toggle_judge')] - public function toggleJudgeAction(Request $request, string $langId): Response - { + public function toggleJudgeAction( + RouterInterface $router, + Request $request, + string $langId + ): Response { $language = $this->em->getRepository(Language::class)->find($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } - $enabled = $request->request->getBoolean('allow_judge'); + $enabled = $request->request->getBoolean('value'); $language->setAllowJudge($enabled); $this->em->flush(); @@ -238,7 +256,28 @@ public function toggleJudgeAction(Request $request, string $langId): Response } $this->dj->auditlog('language', $langId, 'set allow judge', - $request->request->getBoolean('allow_judge') ? 'yes' : 'no'); + $request->request->getBoolean('value') ? 'yes' : 'no'); + return $this->redirectToLocalReferrer( + $router, + $request, + $this->generateUrl('jury_language', ['langId' => $langId]) + ); + } + + #[Route(path: '/{langId}/toggle-filter-compiler-flags', name: 'jury_language_toggle_filter_compiler_files')] + public function toggleFilterCompilerFlagsAction(Request $request, string $langId): Response + { + $language = $this->em->getRepository(Language::class)->find($langId); + if (!$language) { + throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); + } + + $enabled = $request->request->getBoolean('value'); + $language->setFilterCompilerFiles($enabled); + $this->em->flush(); + + $this->dj->auditlog('language', $langId, 'set filter compiler flags', + $request->request->getBoolean('value') ? 'yes' : 'no'); return $this->redirectToRoute('jury_language', ['langId' => $langId]); } @@ -260,8 +299,7 @@ public function editAction(Request $request, string $langId): Response if ($language->getExtensions()) { $language->setExtensions(array_values($language->getExtensions())); } - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $language, - $language->getLangid(), false); + $this->saveEntity($language, $language->getLangid(), false); if ($language->getAllowJudge()) { $this->dj->unblockJudgeTasksForLanguage($langId); } @@ -283,8 +321,7 @@ public function deleteAction(Request $request, string $langId): Response throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$language], $this->generateUrl('jury_languages') + return $this->deleteEntities($request, [$language], $this->generateUrl('jury_languages') ); } diff --git a/webapp/src/Controller/Jury/PrintController.php b/webapp/src/Controller/Jury/PrintController.php index 430648dc876..26bb5416574 100644 --- a/webapp/src/Controller/Jury/PrintController.php +++ b/webapp/src/Controller/Jury/PrintController.php @@ -7,12 +7,14 @@ use App\Form\Type\PrintType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -21,10 +23,14 @@ class PrintController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, - protected readonly ConfigurationService $config - ) {} + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + protected readonly ConfigurationService $config, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_print')] public function showAction(Request $request): Response diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 81d913060d9..6f810122b2e 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -34,11 +34,13 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Yaml\Yaml; use ZipArchive; @@ -50,14 +52,16 @@ class ProblemController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, protected readonly SubmissionService $submissionService, - protected readonly ImportProblemService $importProblemService - ) {} + protected readonly ImportProblemService $importProblemService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_problems')] public function indexAction(): Response @@ -70,9 +74,16 @@ public function indexAction(): Response ->groupBy('p.probid') ->getQuery()->getResult(); + $badgeTitle = ''; + $currentContest = $this->dj->getCurrentContest(); + if ($currentContest !== null) { + $badgeTitle = 'in ' . $currentContest->getShortname(); + } $table_fields = [ 'probid' => ['title' => 'ID', 'sort' => true, 'default_sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], + 'badges' => ['title' => $badgeTitle, 'sort' => false], 'num_contests' => ['title' => '# contests', 'sort' => true], 'timelimit' => ['title' => 'time limit', 'sort' => true], 'memlimit' => ['title' => 'memory limit', 'sort' => true], @@ -80,13 +91,6 @@ public function indexAction(): Response 'num_testcases' => ['title' => '# test cases', 'sort' => true], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Problem::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $contestCountData = $this->em->createQueryBuilder() ->from(ContestProblem::class, 'cp') ->select('COUNT(cp.shortname) AS count', 'p.probid') @@ -115,11 +119,11 @@ public function indexAction(): Response } // Create action links - if ($p->getProblemtextType()) { + if ($p->getProblemstatementType()) { $problemactions[] = [ - 'icon' => 'file-' . $p->getProblemtextType(), - 'title' => 'view problem description', - 'link' => $this->generateUrl('jury_problem_text', [ + 'icon' => 'file-' . $p->getProblemstatementType(), + 'title' => 'view problem statement', + 'link' => $this->generateUrl('jury_problem_statement', [ 'probId' => $p->getProbid(), ]) ]; @@ -165,22 +169,41 @@ public function indexAction(): Response } $problemactions[] = $deleteAction; } + $default_memlimit = $this->config->get('memory_limit'); + $default_output_limit = $this->config->get('output_limit'); // Add formatted {mem,output}limit row data for the table. foreach (['memlimit', 'outputlimit'] as $col) { $orig_value = @$problemdata[$col]['value']; if (!isset($orig_value)) { + $value = 'default'; + if ($col == 'memlimit' && !empty($default_memlimit)) { + $value .= ' (' . Utils::printsize(1024 * $default_memlimit) . ')'; + } + if ($col == 'outputlimit' && !empty($default_output_limit)) { + $value .= ' (' . Utils::printsize(1024 * $default_output_limit) . ')'; + } $problemdata[$col] = [ - 'value' => 'default', + 'value' => $value, 'cssclass' => 'disabled', ]; } else { $problemdata[$col] = [ 'value' => Utils::printsize(1024 * $orig_value), 'sortvalue' => $orig_value, + 'cssclass' => 'right', ]; } } + $problemdata['timelimit']['value'] = @$problemdata['timelimit']['value'] . 's'; + $problemdata['timelimit']['cssclass'] = 'right'; + + $contestProblems = $p->getContestProblems()->toArray(); + $badges = []; + if ($this->dj->getCurrentContest() !== null) { + $badges = array_filter($contestProblems, fn($cp) => $cp->getCid() === $this->dj->getCurrentContest()->getCid()); + } + $problemdata['badges'] = ['value' => $badges]; // merge in the rest of the data $problemdata = array_merge($problemdata, [ @@ -240,7 +263,7 @@ public function exportAction(int $problemId): StreamedResponse $problem = $this->em->createQueryBuilder() ->from(Problem::class, 'p') ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest') - ->leftJoin('p.problemTextContent', 'content') + ->leftJoin('p.problemStatementContent', 'content') ->select('p', 'cp', 'content') ->andWhere('p.probid = :problemId') ->setParameter('problemId', $problemId) @@ -298,9 +321,9 @@ public function exportAction(int $problemId): StreamedResponse $zip->addFromString('domjudge-problem.ini', $iniString); $zip->addFromString('problem.yaml', $yamlString); - if (!empty($problem->getProblemtext())) { - $zip->addFromString('problem.' . $problem->getProblemtextType(), - $problem->getProblemtext()); + if (!empty($problem->getProblemstatement())) { + $zip->addFromString('problem.' . $problem->getProblemstatementType(), + $problem->getProblemstatement()); } $compareExecutable = null; @@ -471,8 +494,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'defaultRunExecutable' => (string)$this->config->get('default_run'), 'defaultCompareExecutable' => (string)$this->config->get('default_compare'), 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') === - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'lockedProblem' => $lockedProblem, 'refresh' => [ 'after' => 15, @@ -490,7 +512,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic return $this->render('jury/problem.html.twig', $data); } - #[Route(path: '/{probId<\d+>}/text', name: 'jury_problem_text')] + #[Route(path: '/{probId<\d+>}/statement', name: 'jury_problem_statement')] public function viewTextAction(int $probId): StreamedResponse { $problem = $this->em->getRepository(Problem::class)->find($probId); @@ -498,7 +520,7 @@ public function viewTextAction(int $probId): StreamedResponse throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } - return $problem->getProblemTextStreamedResponse(); + return $problem->getProblemStatementStreamedResponse(); } #[Route(path: '/{probId<\d+>}/testcases', name: 'jury_problem_testcases')] @@ -904,8 +926,7 @@ public function editAction(Request $request, int $probId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $problem, - $problem->getProbid(), false); + $this->saveEntity($problem, $problem->getProbid(), false); return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); } @@ -976,8 +997,7 @@ public function deleteAction(Request $request, int $probId): Response } } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$problem], $this->generateUrl('jury_problems')); + return $this->deleteEntities($request, [$problem], $this->generateUrl('jury_problems')); } #[Route(path: '/attachments/{attachmentId<\d+>}', name: 'jury_attachment_fetch')] @@ -1012,8 +1032,7 @@ public function deleteAttachmentAction(Request $request, int $attachmentId): Res } } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$attachment], $this->generateUrl('jury_problem', ['probId' => $probId])); + return $this->deleteEntities($request, [$attachment], $this->generateUrl('jury_problem', ['probId' => $probId])); } #[IsGranted('ROLE_ADMIN')] @@ -1059,10 +1078,11 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $this->em->persist($problem); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $problem, null, true); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $problem, + fn() => $this->generateUrl('jury_problem', ['probId' => $problem->getProbid()]) + )) { + return $response; } return $this->render('jury/problem_add.html.twig', [ @@ -1126,6 +1146,43 @@ public function requestRemainingRunsWholeProblemAction(string $probId): Redirect return $this->redirectToRoute('jury_problem', ['probId' => $probId]); } + #[Route(path: '/{contestId}/{probId}/toggle/{type}', name: 'jury_problem_toggle')] + public function toggleSubmitAction( + RouterInterface $router, + Request $request, + string $contestId, + string $probId, + string $type + ): Response { + $contestProblem = $this->em->getRepository(ContestProblem::class)->find([ + 'contest' => $contestId, + 'problem' => $probId, + ]); + if (!$contestProblem) { + throw new NotFoundHttpException(sprintf('Problem with ID %s not found for contest %s', $probId, $contestId)); + } + + $value = $request->request->getBoolean('value'); + + switch ($type) { + case 'judge': + $contestProblem->setAllowJudge($value); + $label = 'set allow judge'; + break; + case 'submit': + $contestProblem->setAllowSubmit($value); + $label = 'set allow submit'; + break; + default: + throw new BadRequestHttpException('Unknown toggle type'); + } + $this->em->flush(); + + $id = [$contestProblem->getCid(), $contestProblem->getProbid()]; + $this->dj->auditlog('contest_problem', implode(', ', $id), $label, $value ? 'yes' : 'no'); + return $this->redirectToLocalReferrer($router, $request, $this->generateUrl('jury_problems')); + } + /** * @param array $allMessages */ diff --git a/webapp/src/Controller/Jury/QueueTaskController.php b/webapp/src/Controller/Jury/QueueTaskController.php index e7758575392..39079654c25 100644 --- a/webapp/src/Controller/Jury/QueueTaskController.php +++ b/webapp/src/Controller/Jury/QueueTaskController.php @@ -5,8 +5,11 @@ use App\Controller\BaseController; use App\Entity\JudgeTask; use App\Entity\QueueTask; +use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; @@ -30,8 +33,13 @@ class QueueTaskController extends BaseController JudgeTask::PRIORITY_HIGH => 'thermometer-full', ]; - public function __construct(private readonly EntityManagerInterface $em) - { + public function __construct( + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); } #[Route(path: '', name: 'jury_queue_tasks')] diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index d08b2391292..f9a185bc5ad 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -18,6 +18,7 @@ use App\Form\Type\RejudgingType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\RejudgingService; use App\Service\SubmissionService; use App\Utils\Utils; @@ -26,6 +27,7 @@ use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Request; @@ -43,13 +45,17 @@ class RejudgingController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly RejudgingService $rejudgingService, protected readonly RouterInterface $router, - protected readonly RequestStack $requestStack - ) {} + protected readonly RequestStack $requestStack, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NoResultException @@ -215,6 +221,21 @@ public function viewAction( ->getQuery() ->getOneOrNullResult(); + $disabledProblems = []; + $disabledLangs = []; + foreach ($rejudging->getJudgings() as $judging) { + $submission = $judging->getSubmission(); + $problem = $submission->getContestProblem(); + $language = $submission->getLanguage(); + + if (!$problem->getAllowJudge()) { + $disabledProblems[$submission->getProblemId()] = $submission->getProblem()->getName(); + } + if (!$language->getAllowJudge()) { + $disabledLangs[$submission->getLanguage()->getLangid()] = $submission->getLanguage()->getName(); + } + } + if (!$rejudging) { throw new NotFoundHttpException(sprintf('Rejudging with ID %s not found', $rejudgingId)); } @@ -381,14 +402,15 @@ public function viewAction( 'newverdict' => $newverdict, 'repetitions' => array_column($repetitions, 'rejudgingid'), 'showStatistics' => $showStatistics, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'stats' => $stats, 'refresh' => [ 'after' => 15, 'url' => $request->getRequestUri(), 'ajax' => true, ], + 'disabledProbs' => $disabledProblems, + 'disabledLangs' => $disabledLangs, ]; if ($request->isXmlHttpRequest()) { $data['ajax'] = true; diff --git a/webapp/src/Controller/Jury/ShadowDifferencesController.php b/webapp/src/Controller/Jury/ShadowDifferencesController.php index ac9938ef98f..bb312390403 100644 --- a/webapp/src/Controller/Jury/ShadowDifferencesController.php +++ b/webapp/src/Controller/Jury/ShadowDifferencesController.php @@ -5,6 +5,7 @@ use App\DataTransferObject\SubmissionRestriction; use App\Entity\ExternalContestSource; use App\Service\ConfigurationService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; @@ -16,6 +17,7 @@ use App\Service\DOMJudgeService; use App\Service\SubmissionService; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -27,12 +29,16 @@ class ShadowDifferencesController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly SubmissionService $submissions, protected readonly RequestStack $requestStack, - protected readonly EntityManagerInterface $em - ) {} + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NoResultException @@ -50,13 +56,8 @@ public function indexAction( #[MapQueryParameter] string $local = 'all', ): Response { - $shadowMode = DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; - $dataSource = $this->config->get('data_source'); - if ($dataSource != $shadowMode) { - $this->addFlash('danger', sprintf( - 'Shadow differences only supported when data_source is %d', - $shadowMode - )); + if (!$this->dj->shadowMode()) { + $this->addFlash('danger', 'Shadow differences only supported when shadow_mode is true'); return $this->redirectToRoute('jury_index'); } diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index 590079e06aa..b8ea182f173 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -49,6 +49,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -60,12 +61,16 @@ class SubmissionController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly SubmissionService $submissionService, - protected readonly RouterInterface $router - ) {} + protected readonly RouterInterface $router, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_submissions')] public function indexAction( @@ -118,6 +123,16 @@ public function indexAction( /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $this->submissionService->getSubmissionList($contests, $restrictions, $limit); + $disabledProblems = []; + $disabledLangs = []; + foreach ($submissions as $submission) { + if (!$submission->getContestProblem()->getAllowJudge()) { + $disabledProblems[$submission->getProblemId()] = $submission->getProblem()->getName(); + } + if (!$submission->getLanguage()->getAllowJudge()) { + $disabledLangs[$submission->getLanguage()->getLangid()] = $submission->getLanguage()->getName(); + } + } // Load preselected filters $filters = $this->dj->jsonDecode((string)$this->dj->getCookie('domjudge_submissionsfilter') ?: '[]'); @@ -135,9 +150,10 @@ public function indexAction( 'showContest' => count($contests) > 1, 'hasFilters' => !empty($filters), 'results' => $results, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'showTestcases' => count($submissions) <= $latestCount, + 'disabledProbs' => $disabledProblems, + 'disabledLangs' => $disabledLangs, ]; // For ajax requests, only return the submission list partial. @@ -390,9 +406,10 @@ public function viewAction( $runResult['hostname'] = $firstJudgingRun->getJudgeTask()->getJudgehost()->getHostname(); $runResult['judgehostid'] = $firstJudgingRun->getJudgeTask()->getJudgehost()->getJudgehostid(); } - $runResult['is_output_run_truncated'] = preg_match( + $runResult['is_output_run_truncated_in_db'] = preg_match( '/\[output storage truncated after \d* B\]/', - (string)$runResult['output_run_last_bytes'] + $outputDisplayLimit >= 0 ? + (string)$runResult['output_run_last_bytes'] : (string)$runResult['output_run'] ); if ($firstJudgingRun) { $runResult['testcasedir'] = $firstJudgingRun->getTestcaseDir(); @@ -453,7 +470,7 @@ public function viewAction( } $unjudgableReasons = []; - if ($runsOutstanding) { + if ($runsOutstanding || $submission->getResult() == null) { // Determine if this submission is unjudgable. $numActiveJudgehosts = (int)$this->em->createQueryBuilder() @@ -667,6 +684,29 @@ public function viewForExternalJudgementAction(ExternalJudgement $externalJudgem ]); } + #[Route(path: '/by-contest-and-external-id/{externalContestId}/{externalId}', name: 'jury_submission_by_context_external_id')] + public function viewForContestExternalIdAction(string $externalContestId, string $externalId): Response + { + $contest = $this->em->getRepository(Contest::class)->findOneBy(['externalid' => $externalContestId]); + if ($contest === null) { + throw new NotFoundHttpException(sprintf('No contest found with external ID %s', $externalContestId)); + } + + $submission = $this->em->getRepository(Submission::class) + ->findOneBy([ + 'contest' => $contest, + 'externalid' => $externalId + ]); + + if (!$submission) { + throw new NotFoundHttpException(sprintf('No submission found with external ID %s', $externalId)); + } + + return $this->redirectToRoute('jury_submission', [ + 'submitId' => $submission->getSubmitid(), + ]); + } + #[Route(path: '/by-external-id/{externalId}', name: 'jury_submission_by_external_id')] public function viewForExternalIdAction(string $externalId): RedirectResponse { @@ -696,7 +736,7 @@ public function teamOutputAction(Submission $submission, Contest $contest, Judgi throw new BadRequestHttpException('Integrity problem while fetching team output.'); } if ($run->getOutput() === null) { - throw new BadRequestHttpException('No team output available (yet).'); + throw new NotFoundHttpException('No team output available (yet).'); } $filename = sprintf('p%d.t%d.%s.run%d.team%d.out', $submission->getProblem()->getProbid(), $run->getTestcase()->getRank(), diff --git a/webapp/src/Controller/Jury/TeamAffiliationController.php b/webapp/src/Controller/Jury/TeamAffiliationController.php index e374e89caf9..23d68439c30 100644 --- a/webapp/src/Controller/Jury/TeamAffiliationController.php +++ b/webapp/src/Controller/Jury/TeamAffiliationController.php @@ -25,13 +25,15 @@ class TeamAffiliationController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly AssetUpdateService $assetUpdater - ) {} + protected readonly AssetUpdateService $assetUpdater, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_team_affiliations')] public function indexAction( @@ -51,6 +53,7 @@ public function indexAction( $table_fields = [ 'affilid' => ['title' => 'ID', 'sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'icpcid' => ['title' => 'ICPC ID', 'sort' => true], 'shortname' => ['title' => 'shortname', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true, 'default_sort' => true], @@ -63,13 +66,6 @@ public function indexAction( $table_fields['num_teams'] = ['title' => '# teams', 'sort' => true]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(TeamAffiliation::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $team_affiliations_table = []; foreach ($teamAffiliations as $teamAffiliationData) { @@ -182,8 +178,7 @@ public function editAction(Request $request, int $affilId): Response if ($form->isSubmitted() && $form->isValid()) { $this->assetUpdater->updateAssets($teamAffiliation); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamAffiliation, - $teamAffiliation->getAffilid(), false); + $this->saveEntity($teamAffiliation, $teamAffiliation->getAffilid(), false); return $this->redirectToRoute('jury_team_affiliation', ['affilId' => $teamAffiliation->getAffilid()]); } @@ -202,8 +197,7 @@ public function deleteAction(Request $request, int $affilId): Response throw new NotFoundHttpException(sprintf('Team affiliation with ID %s not found', $affilId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$teamAffiliation], $this->generateUrl('jury_team_affiliations')); + return $this->deleteEntities($request, [$teamAffiliation], $this->generateUrl('jury_team_affiliations')); } #[IsGranted('ROLE_ADMIN')] @@ -216,11 +210,17 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $this->em->persist($teamAffiliation); - $this->assetUpdater->updateAssets($teamAffiliation); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamAffiliation, null, true); - return $this->redirectToRoute('jury_team_affiliation', ['affilId' => $teamAffiliation->getAffilid()]); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $teamAffiliation, + fn() => $this->generateUrl('jury_team_affiliation', ['affilId' => $teamAffiliation->getAffilid()]), + function () use ($teamAffiliation) { + $this->em->persist($teamAffiliation); + $this->assetUpdater->updateAssets($teamAffiliation); + $this->saveEntity($teamAffiliation, null, true); + return null; + } + )) { + return $response; } return $this->render('jury/team_affiliation_add.html.twig', [ diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 2cdba5b9225..18f6255f0b6 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -32,12 +32,14 @@ class TeamCategoryController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, - protected readonly EventLogService $eventLogService - ) {} + KernelInterface $kernel, + protected readonly EventLogService $eventLogService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_team_categories')] public function indexAction(): Response @@ -53,6 +55,7 @@ public function indexAction(): Response ->getQuery()->getResult(); $table_fields = [ 'categoryid' => ['title' => 'ID', 'sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'icpcid' => ['title' => 'ICPC ID', 'sort' => true], 'sortorder' => ['title' => 'sort', 'sort' => true, 'default_sort' => true], 'name' => ['title' => 'name', 'sort' => true], @@ -61,13 +64,6 @@ public function indexAction(): Response 'allow_self_registration' => ['title' => 'self-registration', 'sort' => true], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(TeamCategory::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $team_categories_table = []; foreach ($teamCategories as $teamCategoryData) { @@ -140,8 +136,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'refresh' => [ 'after' => 15, 'url' => $this->generateUrl('jury_team_category', ['categoryId' => $teamCategory->getCategoryid()]), @@ -172,13 +167,12 @@ public function editAction(Request $request, int $categoryId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamCategory, - $teamCategory->getCategoryid(), false); + $this->saveEntity($teamCategory, $teamCategory->getCategoryid(), false); // Also emit an update event for all teams of the category, since the hidden property might have changed $teams = $teamCategory->getTeams(); if (!$teams->isEmpty()) { $teamIds = array_map(fn(Team $team) => $team->getTeamid(), $teams->toArray()); - foreach ($this->contestsForEntity($teamCategory, $this->dj) as $contest) { + foreach ($this->contestsForEntity($teamCategory) as $contest) { $this->eventLogService->log( 'teams', $teamIds, @@ -209,8 +203,7 @@ public function deleteAction(Request $request, int $categoryId): Response throw new NotFoundHttpException(sprintf('Team category with ID %s not found', $categoryId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$teamCategory], $this->generateUrl('jury_team_categories')); + return $this->deleteEntities($request, [$teamCategory], $this->generateUrl('jury_team_categories')); } #[IsGranted('ROLE_ADMIN')] @@ -223,10 +216,11 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $this->em->persist($teamCategory); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamCategory, null, true); - return $this->redirectToRoute('jury_team_category', ['categoryId' => $teamCategory->getCategoryid()]); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $teamCategory, + fn() => $this->generateUrl('jury_team_category', ['categoryId' => $teamCategory->getCategoryid()]) + )) { + return $response; } return $this->render('jury/team_category_add.html.twig', [ @@ -250,7 +244,7 @@ public function requestRemainingRunsWholeTeamCategoryAction(string $categoryId): ->join('t.category', 'tc') ->andWhere('j.valid = true') ->andWhere('j.result != :compiler_error') - ->andWhere('tc.category = :categoryId') + ->andWhere('tc.categoryid = :categoryId') ->setParameter('compiler_error', 'compiler-error') ->setParameter('categoryId', $categoryId); if ($contestId > -1) { diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index ccd2b7e55cd..08741779e26 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -32,13 +32,15 @@ class TeamController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly AssetUpdateService $assetUpdater - ) {} + protected readonly AssetUpdateService $assetUpdater, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_teams')] public function indexAction(): Response @@ -88,6 +90,7 @@ public function indexAction(): Response $table_fields = [ 'teamid' => ['title' => 'ID', 'sort' => true, 'default_sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'label' => ['title' => 'label', 'sort' => true,], 'effective_name' => ['title' => 'name', 'sort' => true,], 'category' => ['title' => 'category', 'sort' => true,], @@ -99,13 +102,6 @@ public function indexAction(): Response 'stats' => ['title' => 'stats', 'sort' => true,], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Team::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $userDataPerTeam = $this->em->createQueryBuilder() ->from(Team::class, 't', 't.teamid') ->leftJoin('t.users', 'u') @@ -295,8 +291,7 @@ public function viewAction( $data['restrictionText'] = $restrictionText; $data['submissions'] = $submissions; $data['submissionCounts'] = $submissionCounts; - $data['showExternalResult'] = $this->config->get('data_source') === - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; + $data['showExternalResult'] = $this->dj->shadowMode(); $data['showContest'] = count($this->dj->getCurrentContests(honorCookie: true)) > 1; if ($request->isXmlHttpRequest()) { @@ -322,9 +317,9 @@ public function editAction(Request $request, int $teamId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + $this->possiblyAddUser($team); $this->assetUpdater->updateAssets($team); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $team, - $team->getTeamid(), false); + $this->saveEntity($team, $team->getTeamid(), false); return $this->redirectToRoute('jury_team', ['teamId' => $team->getTeamid()]); } @@ -343,8 +338,7 @@ public function deleteAction(Request $request, int $teamId): Response throw new NotFoundHttpException(sprintf('Team with ID %s not found', $teamId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$team], $this->generateUrl('jury_teams')); + return $this->deleteEntities($request, [$team], $this->generateUrl('jury_teams')); } #[IsGranted('ROLE_ADMIN')] @@ -357,26 +351,18 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - /** @var User $user */ - $user = $team->getUsers()->first(); - if ($team->getAddUserForTeam() === Team::CREATE_NEW_USER) { - // Create a user for the team. - $user = new User(); - $user->setUsername($team->getNewUsername()); - $team->addUser($user); - // Make sure the user has the team role to make validation work. - $role = $this->em->getRepository(Role::class)->findOneBy(['dj_role' => 'team']); - $user->addUserRole($role); - // Set the user's name to the team name when creating a new user. - $user->setName($team->getEffectiveName()); - } elseif ($team->getAddUserForTeam() === Team::ADD_EXISTING_USER) { - $team->addUser($team->getExistingUser()); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $team, + fn() => $this->generateUrl('jury_team', ['teamId' => $team->getTeamid()]), + function () use ($team) { + $this->possiblyAddUser($team); + $this->em->persist($team); + $this->assetUpdater->updateAssets($team); + $this->saveEntity($team, null, true); + return null; } - $this->em->persist($team); - $this->assetUpdater->updateAssets($team); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $team, null, true); - return $this->redirectToRoute('jury_team', ['teamId' => $team->getTeamid()]); + )) { + return $response; } return $this->render('jury/team_add.html.twig', [ @@ -384,4 +370,25 @@ public function addAction(Request $request): Response 'form' => $form, ]); } + + /** + * Add an existing or new user to a team if configured to do so + */ + protected function possiblyAddUser(Team $team): void + { + if ($team->getAddUserForTeam() === Team::CREATE_NEW_USER) { + // Create a user for the team. + $user = new User(); + $user->setUsername($team->getNewUsername()); + $user->setExternalid($team->getNewUsername()); + $team->addUser($user); + // Make sure the user has the team role to make validation work. + $role = $this->em->getRepository(Role::class)->findOneBy(['dj_role' => 'team']); + $user->addUserRole($role); + // Set the user's name to the team name when creating a new user. + $user->setName($team->getEffectiveName()); + } elseif ($team->getAddUserForTeam() === Team::ADD_EXISTING_USER) { + $team->addUser($team->getExistingUser()); + } + } } diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 7f0278dbafa..10572a73578 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -36,13 +36,15 @@ class UserController extends BaseController public const MIN_PASSWORD_LENGTH = 10; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly TokenStorageInterface $tokenStorage - ) {} + protected readonly TokenStorageInterface $tokenStorage, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_users')] public function indexAction(): Response @@ -58,6 +60,7 @@ public function indexAction(): Response $table_fields = [ 'username' => ['title' => 'username', 'sort' => true, 'default_sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], 'email' => ['title' => 'email', 'sort' => true], 'user_roles' => ['title' => 'roles', 'sort' => true], @@ -70,13 +73,6 @@ public function indexAction(): Response $table_fields['last_ip_address'] = ['title' => 'last IP', 'sort' => true]; $table_fields['status'] = ['title' => '', 'sort' => true]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(User::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $users_table = []; $timeFormat = (string)$this->config->get('time_format'); @@ -190,8 +186,7 @@ public function viewAction(int $userId, SubmissionService $submissionService): R 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') === - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'refresh' => [ 'after' => 3, 'url' => $this->generateUrl('jury_user', ['userId' => $user->getUserid()]), @@ -231,9 +226,7 @@ public function editAction(Request $request, int $userId): Response if ($errorResult = $this->checkPasswordLength($user, $form)) { return $errorResult; } - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $user, - $user->getUserid(), - false); + $this->saveEntity($user, $user->getUserid(), false); // If we save the currently logged in used, update the login token. if ($user->getUserid() === $this->dj->getUser()->getUserid()) { @@ -265,8 +258,7 @@ public function deleteAction(Request $request, int $userId): Response throw new NotFoundHttpException(sprintf('User with ID %s not found', $userId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$user], $this->generateUrl('jury_users')); + return $this->deleteEntities($request, [$user], $this->generateUrl('jury_users')); } #[IsGranted('ROLE_ADMIN')] @@ -285,13 +277,19 @@ public function addAction( $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - if ($errorResult = $this->checkPasswordLength($user, $form)) { - return $errorResult; + if ($response = $this->processAddFormForExternalIdEntity( + $form, $user, + fn() => $this->generateUrl('jury_user', ['userId' => $user->getUserid()]), + function () use ($user, $form) { + if ($errorResult = $this->checkPasswordLength($user, $form)) { + return $errorResult; + } + $this->em->persist($user); + $this->saveEntity($user, null, true); + return null; } - $this->em->persist($user); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $user, null, true); - return $this->redirectToRoute('jury_user', ['userId' => $user->getUserid()]); + )) { + return $response; } return $this->render('jury/user_add.html.twig', [ diff --git a/webapp/src/Controller/Jury/VersionController.php b/webapp/src/Controller/Jury/VersionController.php index 311e918219a..f8c0ab2f8b0 100644 --- a/webapp/src/Controller/Jury/VersionController.php +++ b/webapp/src/Controller/Jury/VersionController.php @@ -5,9 +5,12 @@ use App\Controller\BaseController; use App\Entity\Language; use App\Entity\Version; +use App\Service\DOMJudgeService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -15,7 +18,14 @@ #[Route(path: '/jury/versions')] class VersionController extends BaseController { - public function __construct(private readonly EntityManagerInterface $em) {} + public function __construct( + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_versions')] public function indexAction(): Response diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index fcc6966bb84..f09e2791d0d 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -5,8 +5,10 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Team; +use App\Entity\TeamCategory; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\ScoreboardService; use App\Service\StatisticsService; use Doctrine\ORM\EntityManagerInterface; @@ -19,6 +21,7 @@ use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\RouterInterface; @@ -26,14 +29,19 @@ class PublicController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly ScoreboardService $scoreboardService, protected readonly StatisticsService $stats, - protected readonly EntityManagerInterface $em - ) {} + EntityManagerInterface $em, + EventLogService $eventLog, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLog, $dj, $kernel); + } #[Route(path: '', name: 'public_index')] + #[Route(path: '/scoreboard')] public function scoreboardAction( Request $request, #[MapQueryParameter(name: 'contest')] @@ -41,9 +49,16 @@ public function scoreboardAction( #[MapQueryParameter] ?bool $static = false, ): Response { - $response = new Response(); - $refreshUrl = $this->generateUrl('public_index'); - $contest = $this->dj->getCurrentContest(onlyPublic: true); + $response = new Response(); + $refreshUrl = $this->generateUrl('public_index'); + $contest = $this->dj->getCurrentContest(onlyPublic: true); + $nonPublicContest = $this->dj->getCurrentContest(onlyPublic: false); + if (!$contest && $nonPublicContest && $this->em->getRepository(TeamCategory::class)->count(['allow_self_registration' => 1])) { + // This leaks a little bit of information about the existence of the non-public contest, + // but since self registration is enabled, it's not a big deal. + return $this->redirectToRoute('register'); + } + if ($static) { $refreshParams = [ @@ -168,8 +183,8 @@ public function problemsAction(): Response $this->dj->getTwigDataForProblemsAction($this->stats)); } - #[Route(path: '/problems/{probId<\d+>}/text', name: 'public_problem_text')] - public function problemTextAction(int $probId): StreamedResponse + #[Route(path: '/problems/{probId<\d+>}/statement', name: 'public_problem_statement')] + public function problemStatementAction(int $probId): StreamedResponse { return $this->getBinaryFile($probId, function ( int $probId, @@ -179,7 +194,7 @@ public function problemTextAction(int $probId): StreamedResponse $problem = $contestProblem->getProblem(); try { - return $problem->getProblemTextStreamedResponse(); + return $problem->getProblemStatementStreamedResponse(); } catch (BadRequestHttpException $e) { $this->addFlash('danger', $e->getMessage()); return $this->redirectToRoute('public_problems'); @@ -187,6 +202,16 @@ public function problemTextAction(int $probId): StreamedResponse }); } + #[Route(path: '/problemset', name: 'public_contest_problemset')] + public function contestProblemsetAction(): StreamedResponse + { + $contest = $this->dj->getCurrentContest(onlyPublic: true); + if (!$contest->getFreezeData()->started()) { + throw new NotFoundHttpException('Contest problemset not found or not available'); + } + return $contest->getContestProblemsetStreamedResponse(); + } + /** * @throws NonUniqueResultException */ diff --git a/webapp/src/Controller/RootController.php b/webapp/src/Controller/RootController.php index eb330a17f14..16085edb6e5 100644 --- a/webapp/src/Controller/RootController.php +++ b/webapp/src/Controller/RootController.php @@ -3,12 +3,15 @@ namespace App\Controller; use App\Service\DOMJudgeService; +use App\Service\EventLogService; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Twig\Extra\Markdown\MarkdownRuntime; @@ -16,10 +19,6 @@ #[Route(path: '')] class RootController extends BaseController { - public function __construct(protected readonly DOMJudgeService $dj) - { - } - #[Route(path: '', name: 'root')] public function redirectAction(AuthorizationCheckerInterface $authorizationChecker): RedirectResponse { diff --git a/webapp/src/Controller/SecurityController.php b/webapp/src/Controller/SecurityController.php index e0033e020ef..4b74f801a45 100644 --- a/webapp/src/Controller/SecurityController.php +++ b/webapp/src/Controller/SecurityController.php @@ -105,7 +105,7 @@ public function registerAction( $plainPass = $registration_form->get('plainPassword')->getData(); $password = $passwordHasher->hashPassword($user, $plainPass); $user->setPassword($password); - if ($user->getName() === null) { + if ((string)$user->getName() === '') { $user->setName($user->getUsername()); } diff --git a/webapp/src/Controller/Team/ClarificationController.php b/webapp/src/Controller/Team/ClarificationController.php index ab376d91b2f..35ef682ae9b 100644 --- a/webapp/src/Controller/Team/ClarificationController.php +++ b/webapp/src/Controller/Team/ClarificationController.php @@ -16,6 +16,7 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; @@ -34,12 +35,15 @@ class ClarificationController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly EventLogService $eventLogService, - protected readonly FormFactoryInterface $formFactory - ) {} + protected readonly FormFactoryInterface $formFactory, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NonUniqueResultException diff --git a/webapp/src/Controller/Team/LanguageController.php b/webapp/src/Controller/Team/LanguageController.php index 2ae1df664cb..1eb7b2e7b8a 100644 --- a/webapp/src/Controller/Team/LanguageController.php +++ b/webapp/src/Controller/Team/LanguageController.php @@ -5,10 +5,13 @@ use App\Controller\BaseController; use App\Entity\Language; use App\Service\ConfigurationService; +use App\Service\DOMJudgeService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -21,9 +24,14 @@ class LanguageController extends BaseController { public function __construct( - protected readonly ConfigurationService $config, - protected readonly EntityManagerInterface $em - ) {} + protected readonly ConfigurationService $config, + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'team_languages')] public function languagesAction(): Response diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index 59d1709155a..4c79f9df16c 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -5,6 +5,8 @@ use App\Controller\BaseController; use App\DataTransferObject\SubmissionRestriction; use App\Entity\Clarification; +use App\Entity\Contest; +use App\Entity\ContestProblem; use App\Entity\Language; use App\Form\Type\PrintType; use App\Service\ConfigurationService; @@ -16,6 +18,10 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -36,13 +42,16 @@ class MiscController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly ScoreboardService $scoreboardService, protected readonly SubmissionService $submissionService, - protected readonly EventLogService $eventLogService - ) {} + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NoResultException @@ -175,13 +184,10 @@ public function printAction(Request $request): Response $propertyAccessor = PropertyAccess::createPropertyAccessor(); $team = $this->dj->getUser()->getTeam(); - $externalIdField = $this->eventLogService->externalIdFieldForEntity($team); if ($team->getLabel()) { $teamId = $team->getLabel(); - } elseif ($externalIdField && ($externalId = $propertyAccessor->getValue($team, $externalIdField))) { - $teamId = $externalId; } else { - $teamId = (string)$team->getTeamid(); + $teamId = $team->getExternalid(); } $ret = $this->dj->printFile($realfile, $originalfilename, $langid, $username, $team->getEffectiveName(), $teamId, $team->getLocation()); @@ -211,4 +217,15 @@ public function docsAction(): Response { return $this->render('team/docs.html.twig'); } + + #[Route(path: '/problemset', name: 'team_contest_problemset')] + public function contestProblemsetAction(): StreamedResponse + { + $user = $this->dj->getUser(); + $contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid()); + if (!$contest->getFreezeData()->started()) { + throw new NotFoundHttpException('Contest text not found or not available'); + } + return $contest->getContestProblemsetStreamedResponse(); + } } diff --git a/webapp/src/Controller/Team/ProblemController.php b/webapp/src/Controller/Team/ProblemController.php index a6b7316eff9..c8f3bd6b575 100644 --- a/webapp/src/Controller/Team/ProblemController.php +++ b/webapp/src/Controller/Team/ProblemController.php @@ -7,10 +7,12 @@ use App\Entity\ContestProblem; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\StatisticsService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -27,11 +29,15 @@ class ProblemController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly StatisticsService $stats, - protected readonly EntityManagerInterface $em - ) {} + protected readonly EventLogService $eventLogService, + EntityManagerInterface $em, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NonUniqueResultException @@ -45,8 +51,8 @@ public function problemsAction(): Response } - #[Route(path: '/problems/{probId<\d+>}/text', name: 'team_problem_text')] - public function problemTextAction(int $probId): StreamedResponse + #[Route(path: '/problems/{probId<\d+>}/statement', name: 'team_problem_statement')] + public function problemStatementAction(int $probId): StreamedResponse { return $this->getBinaryFile($probId, function ( int $probId, @@ -56,7 +62,7 @@ public function problemTextAction(int $probId): StreamedResponse $problem = $contestProblem->getProblem(); try { - return $problem->getProblemTextStreamedResponse(); + return $problem->getProblemStatementStreamedResponse(); } catch (BadRequestHttpException $e) { $this->addFlash('danger', $e->getMessage()); return $this->redirectToRoute('team_problems'); diff --git a/webapp/src/Controller/Team/ScoreboardController.php b/webapp/src/Controller/Team/ScoreboardController.php index add34943e4c..add967a227c 100644 --- a/webapp/src/Controller/Team/ScoreboardController.php +++ b/webapp/src/Controller/Team/ScoreboardController.php @@ -6,9 +6,12 @@ use App\Entity\Team; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\ScoreboardService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -23,15 +26,23 @@ class ScoreboardController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly ScoreboardService $scoreboardService, - protected readonly EntityManagerInterface $em - ) {} + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '/scoreboard', name: 'team_scoreboard')] public function scoreboardAction(Request $request): Response { + if (!$this->config->get('enable_ranking')) { + throw new BadRequestHttpException('Scoreboard is not available.'); + } + $user = $this->dj->getUser(); $response = new Response(); $contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid()); @@ -51,6 +62,10 @@ public function scoreboardAction(Request $request): Response #[Route(path: '/team/{teamId<\d+>}', name: 'team_team')] public function teamAction(Request $request, int $teamId): Response { + if (!$this->config->get('enable_ranking')) { + throw new BadRequestHttpException('Scoreboard is not available.'); + } + /** @var Team|null $team */ $team = $this->em->getRepository(Team::class)->find($teamId); if ($team && $team->getCategory() && !$team->getCategory()->getVisible() && $teamId !== $this->dj->getUser()->getTeamId()) { diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index e6cfbf223f1..35e1a382e7c 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -11,11 +11,13 @@ use App\Form\Type\SubmitProblemType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\SubmissionService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -37,12 +39,16 @@ class SubmissionController extends BaseController final public const ALWAYS_SHOW_COMPILE_OUTPUT = 2; public function __construct( - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly SubmissionService $submissionService, - protected readonly DOMJudgeService $dj, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly FormFactoryInterface $formFactory - ) {} + protected readonly FormFactoryInterface $formFactory, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '/submit/{problem}', name: 'team_submit')] public function createAction(Request $request, ?Problem $problem = null): Response diff --git a/webapp/src/DataFixtures/DefaultData/LanguageFixture.php b/webapp/src/DataFixtures/DefaultData/LanguageFixture.php index 7ec96a0b68e..6c82704e406 100644 --- a/webapp/src/DataFixtures/DefaultData/LanguageFixture.php +++ b/webapp/src/DataFixtures/DefaultData/LanguageFixture.php @@ -25,29 +25,29 @@ public function load(ObjectManager $manager): void $data = [ // ID external ID name extensions require entry point allow allow time compile compiler version runner version // entry point description submit judge factor script command command - ['adb', 'ada', 'Ada', ['adb', 'ads'], false, null, false, true, 1, 'adb', '', ''], - ['awk', 'awk', 'AWK', ['awk'], false, null, false, true, 1, 'awk', '', ''], - ['bash', 'bash', 'Bash shell', ['bash'], false, 'Main file', false, true, 1, 'bash', '', ''], + ['adb', 'ada', 'Ada', ['adb', 'ads'], false, null, false, true, 1, 'adb', 'gnatmake --version', ''], + ['awk', 'awk', 'AWK', ['awk'], false, null, false, true, 1, 'awk', 'awk --version', 'awk --version'], + ['bash', 'bash', 'Bash shell', ['bash'], false, 'Main file', false, true, 1, 'bash', 'bash --version', 'bash --version'], ['c', 'c', 'C', ['c'], false, null, true, true, 1, 'c', 'gcc --version', ''], ['cpp', 'cpp', 'C++', ['cpp', 'cc', 'cxx', 'c++'], false, null, true, true, 1, 'cpp', 'g++ --version', ''], - ['csharp', 'csharp', 'C#', ['csharp', 'cs'], false, null, false, true, 1, 'csharp', '', ''], - ['f95', 'f95', 'Fortran', ['f95', 'f90'], false, null, false, true, 1, 'f95', '', ''], - ['hs', 'haskell', 'Haskell', ['hs', 'lhs'], false, null, false, true, 1, 'hs', '', ''], + ['csharp', 'csharp', 'C#', ['csharp', 'cs'], false, null, false, true, 1, 'csharp', 'mcs --version', 'mono --version'], + ['f95', 'f95', 'Fortran', ['f95', 'f90'], false, null, false, true, 1, 'f95', 'gfortran --version', ''], + ['hs', 'haskell', 'Haskell', ['hs', 'lhs'], false, null, false, true, 1, 'hs', 'ghc --version', ''], ['java', 'java', 'Java', ['java'], false, 'Main class', true, true, 1, 'java_javac_detect', 'javac -version', 'java -version'], - ['js', 'javascript', 'JavaScript', ['js'], false, 'Main file', false, true, 1, 'js', '', ''], - ['lua', 'lua', 'Lua', ['lua'], false, null, false, true, 1, 'lua', '', ''], + ['js', 'javascript', 'JavaScript', ['js', 'mjs'], false, 'Main file', false, true, 1, 'js', 'nodejs --version', 'nodejs --version'], + ['lua', 'lua', 'Lua', ['lua'], false, null, false, true, 1, 'lua', 'luac -v', 'lua -v'], ['kt', 'kotlin', 'Kotlin', ['kt'], true, 'Main class', false, true, 1, 'kt', 'kotlinc -version', 'kotlin -version'], - ['pas', 'pascal', 'Pascal', ['pas', 'p'], false, 'Main file', false, true, 1, 'pas', '', ''], - ['pl', 'pl', 'Perl', ['pl'], false, 'Main file', false, true, 1, 'pl', '', ''], - ['plg', 'prolog', 'Prolog', ['plg'], false, 'Main file', false, true, 1, 'plg', '', ''], + ['pas', 'pascal', 'Pascal', ['pas', 'p'], false, 'Main file', false, true, 1, 'pas', 'fpc -iW', ''], + ['pl', 'pl', 'Perl', ['pl'], false, 'Main file', false, true, 1, 'pl', 'perl -v', 'perl -v'], + ['plg', 'prolog', 'Prolog', ['plg'], false, 'Main file', false, true, 1, 'plg', 'swipl --version', ''], ['py3', 'python3', 'Python 3', ['py'], false, 'Main file', true, true, 1, 'py3', 'pypy3 --version', 'pypy3 --version'], ['ocaml', 'ocaml', 'OCaml', ['ml'], false, null, false, true, 1, 'ocaml', 'ocamlopt --version', ''], - ['r', 'r', 'R', ['R'], false, 'Main file', false, true, 1, 'r', '', ''], - ['rb', 'ruby', 'Ruby', ['rb'], false, 'Main file', false, true, 1, 'rb', '', ''], - ['rs', 'rust', 'Rust', ['rs'], false, null, false, true, 1, 'rs', '', ''], - ['scala', 'scala', 'Scala', ['scala'], false, null, false, true, 1, 'scala', '', ''], - ['sh', 'sh', 'POSIX shell', ['sh'], false, 'Main file', false, true, 1, 'sh', '', ''], - ['swift', 'swift', 'Swift', ['swift'], false, 'Main file', false, true, 1, 'swift', '', ''], + ['r', 'r', 'R', ['R'], false, 'Main file', false, true, 1, 'r', 'Rscript --version', 'Rscript --version'], + ['rb', 'ruby', 'Ruby', ['rb'], false, 'Main file', false, true, 1, 'rb', 'ruby --version', 'ruby --version'], + ['rs', 'rust', 'Rust', ['rs'], false, null, false, true, 1, 'rs', 'rustc --version', ''], + ['scala', 'scala', 'Scala', ['scala'], false, null, false, true, 1, 'scala', 'scalac -version', 'scala -version'], + ['sh', 'sh', 'POSIX shell', ['sh'], false, 'Main file', false, true, 1, 'sh', 'md5sum /bin/sh', 'md5sum /bin/sh'], + ['swift', 'swift', 'Swift', ['swift'], false, 'Main file', false, true, 1, 'swift', 'swiftc --version', ''], ]; foreach ($data as $item) { diff --git a/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php b/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php index e4bb3987153..beadaa8f99c 100644 --- a/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php +++ b/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php @@ -26,7 +26,7 @@ public function load(ObjectManager $manager): void /** @var Contest $contest */ $contest = $manager->getRepository(Contest::class)->findOneBy(['shortname' => 'beforeFreeze']); - + /** @var Problem $problemA */ $problemA = new Problem(); $problemA->setName('U'); @@ -39,9 +39,11 @@ public function load(ObjectManager $manager): void $manager->persist($problemA); $manager->persist($cp); foreach ($submissionData as $index => $submissionItem) { + /** @var Team $team */ + $team = $manager->getRepository(Team::class)->findOneBy(['name' => $submissionItem[0]]); $submission = (new Submission()) ->setContest($contest) - ->setTeam($manager->getRepository(Team::class)->findOneBy(['name' => $submissionItem[0]])) + ->setTeam($team) ->setContestProblem($cp) ->setLanguage($manager->getRepository(Language::class)->find($submissionItem[1])) ->setSubmittime(Utils::now()-2) @@ -57,7 +59,10 @@ public function load(ObjectManager $manager): void /** @var Balloon $balloon */ $balloon = new Balloon(); $balloon->setSubmission($submission) - ->setDone(false); + ->setDone(false) + ->setTeam($team) + ->setContest($contest) + ->setProblem($problemA); $manager->persist($balloon); } $submission->addJudging($judging); diff --git a/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsOneFixture.php b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsOneFixture.php new file mode 100644 index 00000000000..8207c6f070f --- /dev/null +++ b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsOneFixture.php @@ -0,0 +1,16 @@ +getRepository(Team::class)->findOneBy(['name' => 'DOMjudge']); + $team->setExternalid('1'); + $manager->flush(); + } +} diff --git a/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsTwoFixture.php b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsTwoFixture.php new file mode 100644 index 00000000000..50ee1ffc376 --- /dev/null +++ b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsTwoFixture.php @@ -0,0 +1,16 @@ +getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); + $team->setExternalid('2'); + $manager->flush(); + } +} diff --git a/webapp/src/DataTransferObject/Award.php b/webapp/src/DataTransferObject/Award.php index eb5f2ade8d5..fa3fc8faeb8 100644 --- a/webapp/src/DataTransferObject/Award.php +++ b/webapp/src/DataTransferObject/Award.php @@ -11,7 +11,7 @@ class Award */ public function __construct( public readonly string $id, - public readonly string $citation, + public readonly ?string $citation, #[Serializer\Type('array')] public readonly array $teamIds, ) {} diff --git a/webapp/src/DataTransferObject/ContestState.php b/webapp/src/DataTransferObject/ContestState.php index e10d0ef6edc..9296df9b5ec 100644 --- a/webapp/src/DataTransferObject/ContestState.php +++ b/webapp/src/DataTransferObject/ContestState.php @@ -5,11 +5,11 @@ class ContestState { public function __construct( - public readonly ?string $started, - public readonly ?string $ended, - public readonly ?string $frozen, - public readonly ?string $thawed, - public readonly ?string $finalized, - public readonly ?string $endOfUpdates, + public readonly ?string $started = null, + public readonly ?string $ended = null, + public readonly ?string $frozen = null, + public readonly ?string $thawed = null, + public readonly ?string $finalized = null, + public readonly ?string $endOfUpdates = null, ) {} } diff --git a/webapp/src/DataTransferObject/ResultRow.php b/webapp/src/DataTransferObject/ResultRow.php new file mode 100644 index 00000000000..6f85e40a210 --- /dev/null +++ b/webapp/src/DataTransferObject/ResultRow.php @@ -0,0 +1,40 @@ + $this->teamId, + 'rank' => $this->rank, + 'award' => $this->award, + 'num_solved' => $this->numSolved, + 'total_time' => $this->totalTime, + 'time_of_last_submission' => $this->timeOfLastSubmission, + 'group_winner' => $this->groupWinner, + ]; + } +} diff --git a/webapp/src/DataTransferObject/Scoreboard/Problem.php b/webapp/src/DataTransferObject/Scoreboard/Problem.php index 4f0d3638256..b83613aa062 100644 --- a/webapp/src/DataTransferObject/Scoreboard/Problem.php +++ b/webapp/src/DataTransferObject/Scoreboard/Problem.php @@ -9,7 +9,7 @@ class Problem { public function __construct( #[Serializer\Groups([ARC::GROUP_NONSTRICT])] - public readonly string $label, + public readonly ?string $label, public readonly string $problemId, public readonly int $numJudged, public readonly int $numPending, diff --git a/webapp/src/DataTransferObject/Shadowing/ContestEvent.php b/webapp/src/DataTransferObject/Shadowing/ContestEvent.php index a4161fd6a31..6f8dbabbe2a 100644 --- a/webapp/src/DataTransferObject/Shadowing/ContestEvent.php +++ b/webapp/src/DataTransferObject/Shadowing/ContestEvent.php @@ -12,7 +12,7 @@ public function __construct( public readonly int $penaltyTime, public readonly ?string $formalName, public readonly ?string $startTime, - public readonly ?int $countdownPauseTime, + public readonly ?string $countdownPauseTime, public readonly ?string $scoreboardFreezeDuration, public readonly ?string $scoreboardThawTime, ) {} diff --git a/webapp/src/DataTransferObject/Shadowing/EventType.php b/webapp/src/DataTransferObject/Shadowing/EventType.php index 24d06b720a9..dfa66f332ed 100644 --- a/webapp/src/DataTransferObject/Shadowing/EventType.php +++ b/webapp/src/DataTransferObject/Shadowing/EventType.php @@ -12,6 +12,7 @@ enum EventType: string case JUDGEMENTS = 'judgements'; case JUDGEMENT_TYPES = 'judgement-types'; case LANGUAGES = 'languages'; + case MAP_INFO = 'map-info'; case ORGANIZATIONS = 'organizations'; case PERSONS = 'persons'; case PROBLEMS = 'problems'; diff --git a/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php b/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php index 2aa47e6e279..7c5b76d348c 100644 --- a/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php +++ b/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php @@ -7,7 +7,7 @@ class ProblemEvent implements EventData public function __construct( public readonly string $id, public readonly string $name, - public readonly int $timeLimit, + public readonly float $timeLimit, public readonly ?string $label, public readonly ?string $rgb, ) {} diff --git a/webapp/src/DataTransferObject/TeamCategoryPut.php b/webapp/src/DataTransferObject/TeamCategoryPut.php new file mode 100644 index 00000000000..982f584e256 --- /dev/null +++ b/webapp/src/DataTransferObject/TeamCategoryPut.php @@ -0,0 +1,22 @@ +externalid + ); + parent::__construct($message); + } +} diff --git a/webapp/src/Doctrine/ExternalIdAssigner.php b/webapp/src/Doctrine/ExternalIdAssigner.php new file mode 100644 index 00000000000..c7e7ac3b6e1 --- /dev/null +++ b/webapp/src/Doctrine/ExternalIdAssigner.php @@ -0,0 +1,60 @@ +getObject(); + if ((!$entity instanceof ExternalIdFromInternalIdInterface) && (!$entity instanceof CalculatedExternalIdBasedOnRelatedFieldInterface)) { + return; + } + + if ($entity->getExternalId()) { + return; + } + + $entityClass = get_class($entity); + if ($entity instanceof CalculatedExternalIdBasedOnRelatedFieldInterface) { + $externalid = $entity->getCalculatedExternalId(); + } else { + $metadata = $this->em->getClassMetadata($entityClass); + $primaryKeyField = $metadata->getSingleIdentifierFieldName(); + $externalid = (string)$metadata->getFieldValue($entity, $primaryKeyField); + if ($entity instanceof PrefixedExternalIdInterface) { + $externalid = 'dj-' . $externalid; + } elseif ($this->dj->shadowMode() && $entity instanceof PrefixedExternalIdInShadowModeInterface) { + $externalid = 'dj-' . $externalid; + } + } + + // Check if there is already an entity with that external ID + $existingEntity = $this->em->getRepository($entityClass)->findOneBy(['externalid' => $externalid]); + if ($existingEntity) { + throw new ExternalIdAlreadyExistsException($entityClass, $externalid); + } + + $entity->setExternalId($externalid); + + // Note: flushing in postPersist is not safe in general, but we make sure we only do this + // once, so we can't have an infinite loop here. + $this->em->flush(); + } +} diff --git a/webapp/src/Entity/Balloon.php b/webapp/src/Entity/Balloon.php index 2fc154331be..bb4960f83f2 100644 --- a/webapp/src/Entity/Balloon.php +++ b/webapp/src/Entity/Balloon.php @@ -2,6 +2,7 @@ namespace App\Entity; use Doctrine\ORM\Mapping as ORM; +use JMS\Serializer\Annotation as Serializer; /** * Balloons to be handed out. @@ -13,6 +14,7 @@ 'comment' => 'Balloons to be handed out', ])] #[ORM\Index(columns: ['submitid'], name: 'submitid')] +#[ORM\UniqueConstraint(name: 'unique_problem', columns: ['cid', 'teamid', 'probid'])] class Balloon { #[ORM\Id] @@ -27,6 +29,21 @@ class Balloon #[ORM\JoinColumn(name: 'submitid', referencedColumnName: 'submitid', onDelete: 'CASCADE')] private Submission $submission; + #[ORM\ManyToOne] + #[ORM\JoinColumn(name: 'teamid', referencedColumnName: 'teamid', onDelete: 'CASCADE')] + #[Serializer\Exclude] + private Team $team; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(name: 'probid', referencedColumnName: 'probid', onDelete: 'CASCADE')] + #[Serializer\Exclude] + private Problem $problem; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')] + #[Serializer\Exclude] + private Contest $contest; + public function getBalloonid(): int { return $this->balloonid; @@ -53,4 +70,37 @@ public function getSubmission(): Submission { return $this->submission; } + + public function getTeam(): Team + { + return $this->team; + } + + public function setTeam(Team $team): Balloon + { + $this->team = $team; + return $this; + } + + public function getProblem(): Problem + { + return $this->problem; + } + + public function setProblem(Problem $problem): Balloon + { + $this->problem = $problem; + return $this; + } + + public function getContest(): Contest + { + return $this->contest; + } + + public function setContest(Contest $contest): Balloon + { + $this->contest = $contest; + return $this; + } } diff --git a/webapp/src/Entity/BaseApiEntity.php b/webapp/src/Entity/BaseApiEntity.php index 791edd8c6d5..1ff4d4f4d3f 100644 --- a/webapp/src/Entity/BaseApiEntity.php +++ b/webapp/src/Entity/BaseApiEntity.php @@ -9,21 +9,4 @@ */ abstract class BaseApiEntity { - /** - * Get the API ID field name for this entity. - */ - public function getApiIdField(EventLogService $eventLogService): string - { - return $eventLogService->apiIdFieldForEntity($this); - } - - /** - * Get the API ID for this entity. - */ - public function getApiId(EventLogService $eventLogService): string - { - $field = $eventLogService->apiIdFieldForEntity($this); - $method = 'get'.ucfirst($field); - return (string)$this->{$method}(); - } } diff --git a/webapp/src/Entity/CalculatedExternalIdBasedOnRelatedFieldInterface.php b/webapp/src/Entity/CalculatedExternalIdBasedOnRelatedFieldInterface.php new file mode 100644 index 00000000000..6ec12ac1e9c --- /dev/null +++ b/webapp/src/Entity/CalculatedExternalIdBasedOnRelatedFieldInterface.php @@ -0,0 +1,12 @@ + [null, 190]] )] #[UniqueEntity(fields: 'externalid')] -class Clarification extends BaseApiEntity implements ExternalRelationshipEntityInterface +class Clarification extends BaseApiEntity implements + HasExternalIdInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInShadowModeInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Clarification ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('clarid')] + #[Serializer\Groups([ARC::GROUP_RESTRICTED_NONSTRICT])] protected int $clarid; #[ORM\Column( @@ -48,7 +51,7 @@ class Clarification extends BaseApiEntity implements ExternalRelationshipEntityI ] )] #[OA\Property(nullable: true)] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( @@ -236,13 +239,25 @@ public function getProblem(): ?Problem return $this->problem; } + public function getContestProblem(): ?ContestProblem + { + if (!$this->problem) { + return null; + } + return $this->contest->getContestProblem($this->problem); + } + + public function getProblemId(): ?int + { + return $this->getProblem()?->getProbid(); + } + #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('problem_id')] - #[Serializer\Type('string')] - public function getProblemId(): ?int + public function getApiProblemId(): ?string { - return $this->getProblem()?->getProbid(); + return $this->getProblem()?->getExternalid(); } public function setContest(?Contest $contest = null): Clarification @@ -270,10 +285,9 @@ public function getInReplyTo(): ?Clarification #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('reply_to_id')] - #[Serializer\Type('string')] - public function getInReplyToId(): ?int + public function getInReplyToId(): ?string { - return $this->getInReplyTo()?->getClarid(); + return $this->getInReplyTo()?->getExternalid(); } public function addReply(Clarification $reply): Clarification @@ -304,10 +318,9 @@ public function getSender(): ?Team #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('from_team_id')] - #[Serializer\Type('string')] - public function getSenderId(): ?int + public function getSenderId(): ?string { - return $this->getSender()?->getTeamid(); + return $this->getSender()?->getExternalid(); } public function setRecipient(?Team $recipient = null): Clarification @@ -324,29 +337,9 @@ public function getRecipient(): ?Team #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('to_team_id')] - #[Serializer\Type('string')] - public function getRecipientId(): ?int + public function getRecipientId(): ?string { - return $this->getRecipient()?->getTeamid(); - } - - /** - * Get the entities to check for external ID's while serializing. - * - * This method should return an array with as keys the JSON field names and as values the actual entity - * objects that the SetExternalIdVisitor should check for applicable external ID's - * - * @return array{from_team_id: Team|null, to_team_id: Team|null, - * problem_id: Problem|null, reply_to_id: Clarification|null} - */ - public function getExternalRelationships(): array - { - return [ - 'from_team_id' => $this->getSender(), - 'to_team_id' => $this->getRecipient(), - 'problem_id' => $this->getProblem(), - 'reply_to_id' => $this->getInReplyTo() - ]; + return $this->getRecipient()?->getExternalid(); } public function getSummary(): string diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index b25b7874341..936c0056291 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -4,6 +4,7 @@ use App\Controller\API\AbstractRestController as ARC; use App\DataTransferObject\ContestState; +use App\DataTransferObject\FileWithName; use App\DataTransferObject\ImageFile; use App\Utils\FreezeData; use App\Utils\Utils; @@ -19,6 +20,8 @@ use OpenApi\Attributes as OA; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -47,23 +50,25 @@ )] #[UniqueEntity(fields: 'shortname')] #[UniqueEntity(fields: 'externalid')] -class Contest extends BaseApiEntity implements AssetEntityInterface +class Contest extends BaseApiEntity implements + HasExternalIdInterface, + AssetEntityInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInterface { final public const STARTTIME_UPDATE_MIN_SECONDS_BEFORE = 30; #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Contest ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $cid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Contest ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\SerializedName('external_id')] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column(options: ['comment' => 'Descriptive name'])] @@ -313,6 +318,21 @@ class Contest extends BaseApiEntity implements AssetEntityInterface #[Serializer\Exclude] private bool $isLocked = false; + #[Assert\File] + #[Serializer\Exclude] + private ?UploadedFile $contestProblemsetFile = null; + + #[Serializer\Exclude] + private bool $clearContestProblemset = false; + + #[ORM\Column( + length: 4, + nullable: true, + options: ['comment' => 'File type of contest problemset document'] + )] + #[Serializer\Exclude] + private ?string $contestProblemsetType = null; + /** * @var Collection */ @@ -392,17 +412,39 @@ class Contest extends BaseApiEntity implements AssetEntityInterface #[Serializer\Exclude] private ?ImageFile $bannerForApi = null; + /** + * @var Collection + * + * We use a OneToMany instead of a OneToOne here, because otherwise this + * relation will always be loaded. See the commit message of commit + * 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation + */ + #[ORM\OneToMany( + mappedBy: 'contest', + targetEntity: ContestProblemsetContent::class, + cascade: ['persist'], + orphanRemoval: true + )] + #[Serializer\Exclude] + private Collection $contestProblemsetContent; + + // This field gets filled by the contest visitor with a data transfer + // object that represents the contest problemset document. + #[Serializer\Exclude] + private ?FileWithName $problemsetForApi = null; + public function __construct() { - $this->problems = new ArrayCollection(); - $this->teams = new ArrayCollection(); - $this->removedIntervals = new ArrayCollection(); - $this->clarifications = new ArrayCollection(); - $this->submissions = new ArrayCollection(); - $this->internal_errors = new ArrayCollection(); - $this->team_categories = new ArrayCollection(); - $this->medal_categories = new ArrayCollection(); - $this->externalContestSources = new ArrayCollection(); + $this->problems = new ArrayCollection(); + $this->teams = new ArrayCollection(); + $this->removedIntervals = new ArrayCollection(); + $this->clarifications = new ArrayCollection(); + $this->submissions = new ArrayCollection(); + $this->internal_errors = new ArrayCollection(); + $this->team_categories = new ArrayCollection(); + $this->medal_categories = new ArrayCollection(); + $this->externalContestSources = new ArrayCollection(); + $this->contestProblemsetContent = new ArrayCollection(); } public function getCid(): ?int @@ -873,6 +915,16 @@ public function getProblems(): Collection return $this->problems; } + public function getContestProblem(Problem $problem): ?ContestProblem + { + foreach ($this->getProblems() as $contestProblem) { + if ($contestProblem->getProblem() === $problem) { + return $contestProblem; + } + } + return null; + } + public function addClarification(Clarification $clarification): Contest { $this->clarifications[] = $clarification; @@ -1053,6 +1105,11 @@ public function getDataForJuryInterface(): array $resultItem = []; $method = sprintf('get%stime', ucfirst($time)); $timeValue = $this->{$method}(); + $timeValueString = ''; + if ($time !== 'finalize') { + $method = sprintf('get%stimeString', ucfirst($time)); + $timeValueString = $this->{$method}(); + } if ($time === 'start' && !$this->getStarttimeEnabled()) { $resultItem['icon'] = 'ellipsis-h'; $timeValue = $this->getStarttime(false); @@ -1069,7 +1126,10 @@ public function getDataForJuryInterface(): array } $resultItem['label'] = sprintf('%s time', ucfirst($time)); - $resultItem['time'] = Utils::printtime($timeValue, 'Y-m-d H:i:s (T)'); + $resultItem['time'] = $timeValueString; + if (empty($resultItem['time'])) { + $resultItem['time'] = Utils::printtime($timeValue, 'Y-m-d H:i:s (T)'); + } if ($time === 'start' && !$this->getStarttimeEnabled()) { $resultItem['class'] = 'ignore'; } @@ -1348,6 +1408,57 @@ public function addExternalContestSource(ExternalContestSource $externalContestS return $this; } + public function setContestProblemsetContent(?ContestProblemsetContent $content): self + { + $this->contestProblemsetContent = new ArrayCollection(); + if ($content) { + $this->contestProblemsetContent->add($content); + $content->setContest($this); + } + + return $this; + } + + public function getContestProblemsetContent(): ?ContestProblemsetContent + { + return $this->contestProblemsetContent->first() ?: null; + } + + #[ORM\PrePersist] + #[ORM\PreUpdate] + public function processContestProblemset(): void + { + if ($this->isClearContestProblemset()) { + $this + ->setContestProblemsetContent(null) + ->setContestProblemsetType(null); + } elseif ($this->getContestProblemsetFile()) { + $content = file_get_contents($this->getContestProblemsetFile()->getRealPath()); + $clientName = $this->getContestProblemsetFile()->getClientOriginalName(); + $contestProblemsetType = Utils::getTextType($clientName, $this->getContestProblemsetFile()->getRealPath()); + + if (!isset($contestProblemsetType)) { + throw new Exception('Contest problemset has unknown file type.'); + } + + $contestProblemsetContent = (new ContestProblemsetContent()) + ->setContent($content); + $this + ->setContestProblemsetContent($contestProblemsetContent) + ->setContestProblemsetType($contestProblemsetType); + } + } + + public function getContestProblemsetStreamedResponse(): StreamedResponse + { + return Utils::getTextStreamedResponse( + $this->getContestProblemsetType(), + new BadRequestHttpException(sprintf('Contest c%d problemset has unknown type', $this->getCid())), + sprintf('contest-%s.%s', $this->getShortname(), $this->getContestProblemsetType()), + $this->getContestProblemset() + ); + } + public function getBannerFile(): ?UploadedFile { return $this->bannerFile; @@ -1391,6 +1502,50 @@ public function isClearAsset(string $property): ?bool }; } + public function setContestProblemsetFile(?UploadedFile $contestProblemsetFile): Contest + { + $this->contestProblemsetFile = $contestProblemsetFile; + + // Clear the contest text to make sure the entity is modified. + $this->setContestProblemsetContent(null); + + return $this; + } + + public function setClearContestProblemset(bool $clearContestProblemset): Contest + { + $this->clearContestProblemset = $clearContestProblemset; + $this->setContestProblemsetContent(null); + + return $this; + } + + public function getContestProblemset(): ?string + { + return $this->getContestProblemsetContent()?->getContent(); + } + + public function getContestProblemsetFile(): ?UploadedFile + { + return $this->contestProblemsetFile; + } + + public function isClearContestProblemset(): bool + { + return $this->clearContestProblemset; + } + + public function setContestProblemsetType(?string $contestProblemsetType): Contest + { + $this->contestProblemsetType = $contestProblemsetType; + return $this; + } + + public function getContestProblemsetType(): ?string + { + return $this->contestProblemsetType; + } + public function setPenaltyTimeForApi(?int $penaltyTimeForApi): Contest { $this->penaltyTimeForApi = $penaltyTimeForApi; @@ -1419,4 +1574,21 @@ public function getBannerForApi(): array { return array_filter([$this->bannerForApi]); } + + public function setProblemsetForApi(?FileWithName $problemsetForApi = null): void + { + $this->problemsetForApi = $problemsetForApi; + } + + /** + * @return FileWithName[] + */ + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('problemset')] + #[Serializer\Type('array')] + #[Serializer\Exclude(if: 'object.getProblemsetForApi() === []')] + public function getProblemsetForApi(): array + { + return array_filter([$this->problemsetForApi]); + } } diff --git a/webapp/src/Entity/ContestProblem.php b/webapp/src/Entity/ContestProblem.php index 74591a1a961..238d48e04d2 100644 --- a/webapp/src/Entity/ContestProblem.php +++ b/webapp/src/Entity/ContestProblem.php @@ -1,6 +1,7 @@ [null, 190]])] #[Serializer\VirtualProperty( - name: 'id', + name: 'probid', exp: 'object.getProblem().getProbid()', - options: [new Serializer\Type('string')] + options: [new Serializer\Groups([ARC::GROUP_NONSTRICT])] )] + #[Serializer\VirtualProperty( name: 'short_name', exp: 'object.getShortname()', - options: [new Serializer\Groups(['Nonstrict']), new Serializer\Type('string')] + options: [new Serializer\Groups([ARC::GROUP_NONSTRICT]), new Serializer\Type('string')] )] -class ContestProblem +class ContestProblem extends BaseApiEntity { #[ORM\Column(options: [ 'comment' => 'Unique problem ID within contest, used to sort problems in the scoreboard and typically a single letter', @@ -256,11 +258,6 @@ public function getExternalId(): string return $this->getProblem()->getExternalid(); } - public function getApiId(EventLogService $eventLogService): string - { - return $this->getProblem()->getApiId($eventLogService); - } - #[Assert\Callback] public function validate(ExecutionContextInterface $context): void { diff --git a/webapp/src/Entity/ContestProblemsetContent.php b/webapp/src/Entity/ContestProblemsetContent.php new file mode 100644 index 00000000000..d3b2ef0a015 --- /dev/null +++ b/webapp/src/Entity/ContestProblemsetContent.php @@ -0,0 +1,51 @@ + 'utf8mb4_unicode_ci', + 'charset' => 'utf8mb4', + 'comment' => 'Stores contents of contest problemset documents', +])] +class ContestProblemsetContent +{ + /** + * We use a ManyToOne instead of a OneToOne here, because otherwise the + * reverse of this relation will always be loaded. See the commit message of commit + * 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation. + */ + #[ORM\Id] + #[ORM\ManyToOne(inversedBy: 'contestProblemsetContent')] + #[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')] + private Contest $contest; + + #[ORM\Column(type: 'blobtext', options: ['comment' => 'Problemset document content'])] + private string $content; + + public function getContest(): Contest + { + return $this->contest; + } + + public function setContest(Contest $contest): self + { + $this->contest = $contest; + + return $this; + } + + public function getContent(): string + { + return $this->content; + } + + public function setContent(string $content): self + { + $this->content = $content; + + return $this; + } +} diff --git a/webapp/src/Entity/ExternalIdFromInternalIdInterface.php b/webapp/src/Entity/ExternalIdFromInternalIdInterface.php new file mode 100644 index 00000000000..37128a78bc6 --- /dev/null +++ b/webapp/src/Entity/ExternalIdFromInternalIdInterface.php @@ -0,0 +1,16 @@ + - */ - public function getExternalRelationships(): array; -} diff --git a/webapp/src/Entity/HasExternalIdInterface.php b/webapp/src/Entity/HasExternalIdInterface.php new file mode 100644 index 00000000000..9313ce0466f --- /dev/null +++ b/webapp/src/Entity/HasExternalIdInterface.php @@ -0,0 +1,11 @@ +submission; } - #[Serializer\VirtualProperty] - #[Serializer\SerializedName('submission_id')] - #[Serializer\Type('string')] public function getSubmissionId(): int { return $this->getSubmission()->getSubmitid(); } + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('submission_id')] + public function getApiSubmissionId(): string + { + return $this->getSubmission()->getExternalid(); + } + public function setContest(?Contest $contest = null): Judging { $this->contest = $contest; @@ -439,19 +443,6 @@ public function getInternalError(): ?InternalError return $this->internalError; } - /** - * Get the entities to check for external ID's while serializing. - * - * This method should return an array with as keys the JSON field names and as values the actual entity - * objects that the SetExternalIdVisitor should check for applicable external ID's. - * - * @return array{submission_id: Submission} - */ - public function getExternalRelationships(): array - { - return ['submission_id' => $this->getSubmission()]; - } - /** * Check whether this judging has started judging */ diff --git a/webapp/src/Entity/Language.php b/webapp/src/Entity/Language.php index b5fc9b2aa96..ef92d53afc6 100644 --- a/webapp/src/Entity/Language.php +++ b/webapp/src/Entity/Language.php @@ -25,7 +25,9 @@ #[ORM\UniqueConstraint(name: 'externalid', columns: ['externalid'], options: ['lengths' => [190]])] #[UniqueEntity(fields: 'langid')] #[UniqueEntity(fields: 'externalid')] -class Language extends BaseApiEntity +class Language extends BaseApiEntity implements + HasExternalIdInterface, + ExternalIdFromInternalIdInterface { #[ORM\Id] #[ORM\Column(length: 32, options: ['comment' => 'Language ID (string)'])] @@ -259,7 +261,7 @@ public function getLangid(): ?string return $this->langid; } - public function setExternalid(string $externalid): Language + public function setExternalid(?string $externalid): Language { $this->externalid = $externalid; return $this; @@ -403,14 +405,15 @@ public function getSubmissions(): Collection public function getAceLanguage(): string { return match ($this->getLangid()) { + 'adb' => 'ada', + 'bash' => 'sh', 'c', 'cpp', 'cxx' => 'c_cpp', - 'pas' => 'pascal', 'hs' => 'haskell', + 'kt' => 'kotlin', + 'pas' => 'pascal', 'pl' => 'perl', - 'bash' => 'sh', - 'py2', 'py3' => 'python', - 'adb' => 'ada', 'plg' => 'prolog', + 'py2', 'py3' => 'python', 'rb' => 'ruby', 'rs' => 'rust', default => $this->getLangid(), diff --git a/webapp/src/Entity/PrefixedExternalIdInShadowModeInterface.php b/webapp/src/Entity/PrefixedExternalIdInShadowModeInterface.php new file mode 100644 index 00000000000..f481c42ec80 --- /dev/null +++ b/webapp/src/Entity/PrefixedExternalIdInShadowModeInterface.php @@ -0,0 +1,16 @@ + 'utf8mb4_bin', ] )] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column(options: ['comment' => 'Descriptive name'])] @@ -93,18 +96,18 @@ class Problem extends BaseApiEntity #[Assert\File] #[Serializer\Exclude] - private ?UploadedFile $problemtextFile = null; + private ?UploadedFile $problemstatementFile = null; #[Serializer\Exclude] - private bool $clearProblemtext = false; + private bool $clearProblemstatement = false; #[ORM\Column( length: 4, nullable: true, - options: ['comment' => 'File type of problem text'] + options: ['comment' => 'File type of problem statement'] )] #[Serializer\Exclude] - private ?string $problemtext_type = null; + private ?string $problemstatement_type = null; /** * @var Collection @@ -146,7 +149,7 @@ class Problem extends BaseApiEntity private Collection $testcases; /** - * @var Collection + * @var Collection * * We use a OneToMany instead of a OneToOne here, because otherwise this * relation will always be loaded. See the commit message of commit @@ -154,12 +157,12 @@ class Problem extends BaseApiEntity */ #[ORM\OneToMany( mappedBy: 'problem', - targetEntity: ProblemTextContent::class, + targetEntity: ProblemStatementContent::class, cascade: ['persist'], orphanRemoval: true )] #[Serializer\Exclude] - private Collection $problemTextContent; + private Collection $problemStatementContent; /** * @var Collection @@ -270,48 +273,48 @@ public function getCombinedRunCompare(): bool return $this->combined_run_compare; } - public function setProblemtextFile(?UploadedFile $problemtextFile): Problem + public function setProblemstatementFile(?UploadedFile $problemstatementFile): Problem { - $this->problemtextFile = $problemtextFile; + $this->problemstatementFile = $problemstatementFile; - // Clear the problem text to make sure the entity is modified. - $this->setProblemTextContent(null); + // Clear the problem statement to make sure the entity is modified. + $this->setProblemStatementContent(null); return $this; } - public function setClearProblemtext(bool $clearProblemtext): Problem + public function setClearProblemstatement(bool $clearProblemstatement): Problem { - $this->clearProblemtext = $clearProblemtext; - $this->setProblemTextContent(null); + $this->clearProblemstatement = $clearProblemstatement; + $this->setProblemStatementContent(null); return $this; } - public function getProblemtext(): ?string + public function getProblemstatement(): ?string { - return $this->getProblemTextContent()?->getContent(); + return $this->getProblemStatementContent()?->getContent(); } - public function getProblemtextFile(): ?UploadedFile + public function getProblemstatementFile(): ?UploadedFile { - return $this->problemtextFile; + return $this->problemstatementFile; } - public function isClearProblemtext(): bool + public function isClearProblemstatement(): bool { - return $this->clearProblemtext; + return $this->clearProblemstatement; } - public function setProblemtextType(?string $problemtextType): Problem + public function setProblemstatementType(?string $problemstatementType): Problem { - $this->problemtext_type = $problemtextType; + $this->problemstatement_type = $problemstatementType; return $this; } - public function getProblemtextType(): ?string + public function getProblemstatementType(): ?string { - return $this->problemtext_type; + return $this->problemstatement_type; } public function setCompareExecutable(?Executable $compareExecutable = null): Problem @@ -343,7 +346,7 @@ public function __construct() $this->clarifications = new ArrayCollection(); $this->contest_problems = new ArrayCollection(); $this->attachments = new ArrayCollection(); - $this->problemTextContent = new ArrayCollection(); + $this->problemStatementContent = new ArrayCollection(); } public function addTestcase(Testcase $testcase): Problem @@ -433,94 +436,55 @@ public function getAttachments(): Collection return $this->attachments; } - public function setProblemTextContent(?ProblemTextContent $content): self + public function setProblemStatementContent(?ProblemStatementContent $content): self { - $this->problemTextContent->clear(); + $this->problemStatementContent = new ArrayCollection(); if ($content) { - $this->problemTextContent->add($content); + $this->problemStatementContent->add($content); $content->setProblem($this); } return $this; } - public function getProblemTextContent(): ?ProblemTextContent + public function getProblemStatementContent(): ?ProblemStatementContent { - return $this->problemTextContent->first() ?: null; + return $this->problemStatementContent->first() ?: null; } #[ORM\PrePersist] #[ORM\PreUpdate] - public function processProblemText(): void + public function processProblemStatement(): void { - if ($this->isClearProblemtext()) { + if ($this->isClearProblemstatement()) { $this - ->setProblemTextContent(null) - ->setProblemtextType(null); - } elseif ($this->getProblemtextFile()) { - $content = file_get_contents($this->getProblemtextFile()->getRealPath()); - $clientName = $this->getProblemtextFile()->getClientOriginalName(); - $problemTextType = null; - - if (strrpos($clientName, '.') !== false) { - $ext = substr($clientName, strrpos($clientName, '.') + 1); - if (in_array($ext, ['txt', 'html', 'pdf'])) { - $problemTextType = $ext; - } - } - if (!isset($problemTextType)) { - $finfo = finfo_open(FILEINFO_MIME); - - [$type] = explode('; ', finfo_file($finfo, $this->getProblemtextFile()->getRealPath())); - - finfo_close($finfo); - - switch ($type) { - case 'application/pdf': - $problemTextType = 'pdf'; - break; - case 'text/html': - $problemTextType = 'html'; - break; - case 'text/plain': - $problemTextType = 'txt'; - break; - } - } - - if (!isset($problemTextType)) { + ->setProblemStatementContent(null) + ->setProblemstatementType(null); + } elseif ($this->getProblemstatementFile()) { + $content = file_get_contents($this->getProblemstatementFile()->getRealPath()); + $clientName = $this->getProblemstatementFile()->getClientOriginalName(); + $problemStatementType = Utils::getTextType($clientName, $this->getProblemstatementFile()->getRealPath()); + + if (!isset($problemStatementType)) { throw new Exception('Problem statement has unknown file type.'); } - $problemTextContent = (new ProblemTextContent()) + $problemStatementContent = (new ProblemStatementContent()) ->setContent($content); $this - ->setProblemTextContent($problemTextContent) - ->setProblemtextType($problemTextType); + ->setProblemStatementContent($problemStatementContent) + ->setProblemstatementType($problemStatementType); } } - public function getProblemTextStreamedResponse(): StreamedResponse + public function getProblemStatementStreamedResponse(): StreamedResponse { - $mimetype = match ($this->getProblemtextType()) { - 'pdf' => 'application/pdf', - 'html' => 'text/html', - 'txt' => 'text/plain', - default => throw new BadRequestHttpException(sprintf('Problem p%d text has unknown type', $this->getProbid())), - }; - - $filename = sprintf('prob-%s.%s', $this->getName(), $this->getProblemtextType()); - $problemText = $this->getProblemtext(); - - $response = new StreamedResponse(); - $response->setCallback(function () use ($problemText) { - echo $problemText; - }); - $response->headers->set('Content-Type', sprintf('%s; name="%s"', $mimetype, $filename)); - $response->headers->set('Content-Disposition', sprintf('inline; filename="%s"', $filename)); - $response->headers->set('Content-Length', (string)strlen($problemText)); - - return $response; + return Utils::getTextStreamedResponse( + $this->getProblemstatementType(), + new BadRequestHttpException(sprintf('Problem p%d statement has unknown type', $this->getProbid())), + sprintf('prob-%s.%s', $this->getName(), $this->getProblemstatementType()), + $this->getProblemstatement() + ); } public function setStatementForApi(?FileWithName $statementForApi = null): void diff --git a/webapp/src/Entity/ProblemTextContent.php b/webapp/src/Entity/ProblemStatementContent.php similarity index 82% rename from webapp/src/Entity/ProblemTextContent.php rename to webapp/src/Entity/ProblemStatementContent.php index 3a4d5377823..906e6851999 100644 --- a/webapp/src/Entity/ProblemTextContent.php +++ b/webapp/src/Entity/ProblemStatementContent.php @@ -8,9 +8,9 @@ #[ORM\Table(options: [ 'collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4', - 'comment' => 'Stores contents of problem texts', + 'comment' => 'Stores contents of problem statement', ])] -class ProblemTextContent +class ProblemStatementContent { /** * We use a ManyToOne instead of a OneToOne here, because otherwise the @@ -18,11 +18,11 @@ class ProblemTextContent * 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation. */ #[ORM\Id] - #[ORM\ManyToOne(inversedBy: 'problemTextContent')] + #[ORM\ManyToOne(inversedBy: 'problemStatementContent')] #[ORM\JoinColumn(name: 'probid', referencedColumnName: 'probid', onDelete: 'CASCADE')] private Problem $problem; - #[ORM\Column(type: 'blobtext', options: ['comment' => 'Text content'])] + #[ORM\Column(type: 'blobtext', options: ['comment' => 'Statement content'])] private string $content; public function getProblem(): Problem diff --git a/webapp/src/Entity/Submission.php b/webapp/src/Entity/Submission.php index 46f12f6f7d8..841565177d3 100644 --- a/webapp/src/Entity/Submission.php +++ b/webapp/src/Entity/Submission.php @@ -36,13 +36,16 @@ options: ['lengths' => [null, 190]] )] #[UniqueEntity(fields: 'externalid')] -class Submission extends BaseApiEntity implements ExternalRelationshipEntityInterface +class Submission extends BaseApiEntity implements + HasExternalIdInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInShadowModeInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Submission ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('submitid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected int $submitid; #[ORM\Column( @@ -53,8 +56,7 @@ class Submission extends BaseApiEntity implements ExternalRelationshipEntityInte ] )] #[OA\Property(nullable: true)] - #[Serializer\SerializedName('external_id')] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( @@ -218,7 +220,6 @@ public function getExternalid(): ?string #[Serializer\VirtualProperty] #[Serializer\SerializedName('language_id')] - #[Serializer\Type('string')] public function getLanguageId(): string { return $this->getLanguage()->getExternalid(); @@ -312,14 +313,18 @@ public function getTeam(): Team return $this->team; } - #[Serializer\VirtualProperty] - #[Serializer\SerializedName('team_id')] - #[Serializer\Type('string')] public function getTeamId(): int { return $this->getTeam()->getTeamid(); } + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('team_id')] + public function getApiTeamId(): string + { + return $this->getTeam()->getExternalid(); + } + public function setUser(?User $user = null): Submission { $this->user = $user; @@ -415,14 +420,18 @@ public function getProblem(): Problem return $this->problem; } - #[Serializer\VirtualProperty] - #[Serializer\SerializedName('problem_id')] - #[Serializer\Type('string')] public function getProblemId(): int { return $this->getProblem()->getProbid(); } + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('problem_id')] + public function getApiProblemId(): string + { + return $this->getProblem()->getExternalid(); + } + public function setContestProblem(?ContestProblem $contestProblem = null): Submission { $this->contest_problem = $contestProblem; @@ -445,23 +454,6 @@ public function getRejudging(): ?Rejudging return $this->rejudging; } - /** - * Get the entities to check for external ID's while serializing. - * - * This method should return an array with as keys the JSON field names and as values the actual entity - * objects that the SetExternalIdVisitor should check for applicable external ID's. - * - * @return array{language_id: Language, problem_id: Problem, team_id: Team|null} - */ - public function getExternalRelationships(): array - { - return [ - 'language_id' => $this->getLanguage(), - 'problem_id' => $this->getProblem(), - 'team_id' => $this->getTeam(), - ]; - } - public function isAfterFreeze(): bool { return $this->getContest()->getFreezetime() !== null && (float)$this->getSubmittime() >= (float)$this->getContest()->getFreezetime(); diff --git a/webapp/src/Entity/Team.php b/webapp/src/Entity/Team.php index d789052f7ad..717ec37eb07 100644 --- a/webapp/src/Entity/Team.php +++ b/webapp/src/Entity/Team.php @@ -26,7 +26,11 @@ #[ORM\UniqueConstraint(name: 'externalid', columns: ['externalid'], options: ['lengths' => [190]])] #[ORM\UniqueConstraint(name: 'label', columns: ['label'])] #[UniqueEntity(fields: 'externalid')] -class Team extends BaseApiEntity implements ExternalRelationshipEntityInterface, AssetEntityInterface +class Team extends BaseApiEntity implements + HasExternalIdInterface, + AssetEntityInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInterface { final public const DONT_ADD_USER = 'dont-add-user'; final public const CREATE_NEW_USER = 'create-new-user'; @@ -35,15 +39,15 @@ class Team extends BaseApiEntity implements ExternalRelationshipEntityInterface, #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Team ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('teamid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $teamid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Team ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( @@ -428,10 +432,9 @@ public function getAffiliation(): ?TeamAffiliation #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('organization_id')] - #[Serializer\Type('string')] - public function getAffiliationId(): ?int + public function getAffiliationId(): ?string { - return $this->getAffiliation()?->getAffilid(); + return $this->getAffiliation()?->getExternalid(); } public function setCategory(?TeamCategory $category = null): Team @@ -567,7 +570,7 @@ public function getUnreadClarifications(): Collection #[Serializer\Type('array')] public function getGroupIds(): array { - return $this->getCategory() ? [$this->getCategory()->getCategoryid()] : []; + return $this->getCategory() ? [$this->getCategory()->getExternalid()] : []; } #[OA\Property(nullable: true)] @@ -597,17 +600,6 @@ public function canViewClarification(Clarification $clarification): bool ($clarification->getSender() === null && $clarification->getRecipient() === null)); } - /** - * @return array{organization_id: TeamAffiliation|null, group_ids: TeamCategory[]} - */ - public function getExternalRelationships(): array - { - return [ - 'organization_id' => $this->getAffiliation(), - 'group_ids' => array_values(array_filter([$this->getCategory()])), - ]; - } - #[Assert\Callback] public function validate(ExecutionContextInterface $context): void { diff --git a/webapp/src/Entity/TeamAffiliation.php b/webapp/src/Entity/TeamAffiliation.php index f1f63a0298b..d77958d8646 100644 --- a/webapp/src/Entity/TeamAffiliation.php +++ b/webapp/src/Entity/TeamAffiliation.php @@ -1,6 +1,7 @@ 'Team affiliation ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('affilid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $affilid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Team affiliation ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( diff --git a/webapp/src/Entity/TeamCategory.php b/webapp/src/Entity/TeamCategory.php index 59f3e49383d..0ae161ff87c 100644 --- a/webapp/src/Entity/TeamCategory.php +++ b/webapp/src/Entity/TeamCategory.php @@ -28,20 +28,24 @@ options: [new Serializer\Type('boolean'), new Serializer\Groups(['Nonstrict'])] )] #[UniqueEntity(fields: 'externalid')] -class TeamCategory extends BaseApiEntity implements Stringable +class TeamCategory extends BaseApiEntity implements + Stringable, + HasExternalIdInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Team category ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('categoryid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $categoryid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Team category ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( diff --git a/webapp/src/Entity/User.php b/webapp/src/Entity/User.php index dffd6ba1116..9a3eee42743 100644 --- a/webapp/src/Entity/User.php +++ b/webapp/src/Entity/User.php @@ -1,6 +1,7 @@ [190]])] #[ORM\UniqueConstraint(name: 'externalid', columns: ['externalid'], options: ['lengths' => [190]])] #[UniqueEntity(fields: 'username', message: "The username '{{ value }}' is already in use.")] -class User extends BaseApiEntity implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface, ExternalRelationshipEntityInterface +class User extends BaseApiEntity implements + UserInterface, + PasswordAuthenticatedUserInterface, + EquatableInterface, + HasExternalIdInterface, + CalculatedExternalIdBasedOnRelatedFieldInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'User ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('userid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] private ?int $userid = null; #[ORM\Column( nullable: true, options: ['comment' => 'User ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column(options: ['comment' => 'User login name'])] @@ -364,13 +370,18 @@ public function getTeamName(): ?string return $this->getTeam()?->getEffectiveName(); } + public function getTeamId(): ?int + { + return $this->getTeam()?->getTeamid(); + } + #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('team_id')] #[Serializer\Type('string')] - public function getTeamId(): ?int + public function getApiTeamId(): ?string { - return $this->getTeam()?->getTeamid(); + return $this->getTeam()?->getExternalid(); } public function __construct() @@ -500,13 +511,8 @@ public function getUserIdentifier(): string return $this->getUsername(); } - /** - * @return array{team_id: Team|null} - */ - public function getExternalRelationships(): array + public function getCalculatedExternalId(): string { - return [ - 'team_id' => $this->getTeam(), - ]; + return $this->getUsername(); } } diff --git a/webapp/src/EventListener/ApiHeadersListener.php b/webapp/src/EventListener/ApiHeadersListener.php deleted file mode 100644 index 8c06d6f65e9..00000000000 --- a/webapp/src/EventListener/ApiHeadersListener.php +++ /dev/null @@ -1,21 +0,0 @@ -getRequest(); - // Check if this is an API request. - if (str_starts_with($request->getPathInfo(), '/api')) { - // It is, so add CORS headers. - $response = $event->getResponse(); - $response->headers->set('Access-Control-Allow-Origin', '*'); - } - } -} diff --git a/webapp/src/EventListener/NoSessionCookieForApiListener.php b/webapp/src/EventListener/NoSessionCookieForApiListener.php new file mode 100644 index 00000000000..69e8e644544 --- /dev/null +++ b/webapp/src/EventListener/NoSessionCookieForApiListener.php @@ -0,0 +1,24 @@ +getRequest(); + $response = $event->getResponse(); + if ($request->attributes->get('_firewall_context') === 'security.firewall.map.context.api') { + $response->headers->removeCookie($request->getSession()->getName()); + } + } +} diff --git a/webapp/src/EventListener/ParseFilesForPutListener.php b/webapp/src/EventListener/ParseFilesForPutListener.php new file mode 100644 index 00000000000..0f828f46d08 --- /dev/null +++ b/webapp/src/EventListener/ParseFilesForPutListener.php @@ -0,0 +1,44 @@ +getRequest(); + if ($request->isMethod('PUT')) { + $document = HttpFoundation::convert($request); + // Not all PUT requests are multipart, for example JSON PUT requests aren't. So check + // to see if this is a multipart request and otherwise do nothing. + if (!$document->isMultiPart()) { + return; + } + + foreach ($document->getParts() as $part) { + if (!$part->isFile()) { + continue; + } + + $filename = tempnam(sys_get_temp_dir(), 'dj-put'); + file_put_contents($filename, $part->getBody()); + $uploadedFile = new UploadedFile( + $filename, + $part->getFileName(), + $part->getMimeType(), + test: true // Since it is not a real uploaded file, mark it as test + ); + $request->files->set($part->getName(), $uploadedFile); + } + } + } +} diff --git a/webapp/src/Form/Type/AbstractExternalIdEntityType.php b/webapp/src/Form/Type/AbstractExternalIdEntityType.php index 4c787c05c5b..19d477f85f6 100644 --- a/webapp/src/Form/Type/AbstractExternalIdEntityType.php +++ b/webapp/src/Form/Type/AbstractExternalIdEntityType.php @@ -2,6 +2,7 @@ namespace App\Form\Type; +use App\Entity\ExternalIdFromInternalIdInterface; use App\Service\DOMJudgeService; use App\Service\EventLogService; use Symfony\Component\Form\AbstractType; @@ -24,21 +25,19 @@ public function __construct(protected readonly EventLogService $eventLogService) */ protected function addExternalIdField(FormBuilderInterface $builder, string $entity): void { - if ($this->eventLogService->externalIdFieldForEntity($entity) !== null) { - $builder->add('externalid', TextType::class, [ - 'label' => 'External ID', - 'required' => false, - 'empty_data' => '', - 'constraints' => [ - new Regex( - [ - 'pattern' => DOMJudgeService::EXTERNAL_IDENTIFIER_REGEX, - 'message' => 'Only letters, numbers, dashes, underscores and dots are allowed', - ] - ), - new NotBlank(), - ] - ]); - } + $builder->add('externalid', TextType::class, [ + 'label' => 'External ID', + 'help' => 'Leave empty to generate automatically.', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Regex( + [ + 'pattern' => DOMJudgeService::EXTERNAL_IDENTIFIER_REGEX, + 'message' => 'Only letters, numbers, dashes, underscores and dots are allowed.', + ] + ), + ] + ]); } } diff --git a/webapp/src/Form/Type/ContestType.php b/webapp/src/Form/Type/ContestType.php index bce28faf742..56e3e648dc8 100644 --- a/webapp/src/Form/Type/ContestType.php +++ b/webapp/src/Form/Type/ContestType.php @@ -175,6 +175,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'Delete banner', 'required' => false, ]); + $builder->add('contestProblemsetFile', FileType::class, [ + 'label' => 'Problemset document', + 'required' => false, + 'attr' => [ + 'accept' => 'text/html,text/plain,application/pdf', + ], + ]); + $builder->add('clearContestProblemset', CheckboxType::class, [ + 'label' => 'Delete contest problemset document', + 'required' => false, + ]); $builder->add('warningMessage', TextType::class, [ 'required' => false, 'label' => 'Scoreboard warning message', @@ -197,11 +208,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $contest = $event->getData(); $form = $event->getForm(); - $id = $contest?->getApiId($this->eventLogService); + $id = $contest?->getExternalid(); if (!$contest || !$this->dj->assetPath($id, 'contest')) { $form->remove('clearBanner'); } + + if ($contest && !$contest->getContestProblemset()) { + $form->remove('clearContestProblemset'); + } }); } diff --git a/webapp/src/Form/Type/ExportResultsType.php b/webapp/src/Form/Type/ExportResultsType.php new file mode 100644 index 00000000000..15b45417988 --- /dev/null +++ b/webapp/src/Form/Type/ExportResultsType.php @@ -0,0 +1,78 @@ +em->createQueryBuilder() + ->from(TeamCategory::class, 'c', 'c.categoryid') + ->select('c.sortorder, c.name') + ->where('c.visible = 1') + ->orderBy('c.sortorder') + ->getQuery() + ->getResult(); + $sortOrders = []; + foreach ($teamCategories as $teamCategory) { + $sortOrder = $teamCategory['sortorder']; + if (!array_key_exists($sortOrder, $sortOrders)) { + $sortOrders[$sortOrder] = new stdClass(); + $sortOrders[$sortOrder]->sort_order = $sortOrder; + $sortOrders[$sortOrder]->categories = []; + } + $sortOrders[$sortOrder]->categories[] = $teamCategory['name']; + } + + $builder->add('sortorder', ChoiceType::class, [ + 'choices' => $sortOrders, + 'group_by' => null, + 'choice_label' => fn(stdClass $sortOrder) => sprintf( + '%d with %d %s', + $sortOrder->sort_order, + count($sortOrder->categories), + count($sortOrder->categories) === 1 ? 'category' : 'categories', + ), + 'choice_value' => 'sort_order', + 'choice_attr' => fn(stdClass $sortOrder) => [ + 'data-categories' => json_encode($sortOrder->categories), + ], + 'label' => 'Sort order', + 'help' => '[will be replaced by categories]', + ]); + $builder->add('individually_ranked', ChoiceType::class, [ + 'choices' => [ + 'Yes' => true, + 'No' => false, + ], + 'label' => 'Individually ranked?', + ]); + $builder->add('honors', ChoiceType::class, [ + 'choices' => [ + 'Yes' => true, + 'No' => false, + ], + 'label' => 'Honors?', + ]); + $builder->add('format', ChoiceType::class, [ + 'choices' => [ + 'HTML (display inline)' => 'html_inline', + 'HTML (download)' => 'html_download', + 'TSV' => 'tsv', + ], + 'label' => 'Format', + ]); + $builder->add('export', SubmitType::class, ['icon' => 'fa-download']); + } +} diff --git a/webapp/src/Form/Type/JuryClarificationType.php b/webapp/src/Form/Type/JuryClarificationType.php index c07ef6487d4..376ca92f1aa 100644 --- a/webapp/src/Form/Type/JuryClarificationType.php +++ b/webapp/src/Form/Type/JuryClarificationType.php @@ -23,7 +23,6 @@ public function __construct( private readonly EntityManagerInterface $em, private readonly ConfigurationService $config, private readonly DOMJudgeService $dj, - private readonly EventLogService $eventLogService, ) {} public function buildForm(FormBuilderInterface $builder, array $options): void @@ -119,10 +118,6 @@ private function getTeamLabel(Team $team): string return sprintf('%s (%s)', $team->getEffectiveName(), $team->getLabel()); } - if ($this->eventLogService->externalIdFieldForEntity($team)) { - return sprintf('%s (%s)', $team->getEffectiveName(), $team->getExternalId()); - } - - return sprintf('%s (t%s)', $team->getEffectiveName(), $team->getTeamid()); + return sprintf('%s (%s)', $team->getEffectiveName(), $team->getExternalId()); } } diff --git a/webapp/src/Form/Type/ProblemType.php b/webapp/src/Form/Type/ProblemType.php index 1d41666f862..736d7ec2884 100644 --- a/webapp/src/Form/Type/ProblemType.php +++ b/webapp/src/Form/Type/ProblemType.php @@ -38,15 +38,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help' => 'leave empty for default', 'input_group_after' => 'kB', ]); - $builder->add('problemtextFile', FileType::class, [ - 'label' => 'Problem text', + $builder->add('problemstatementFile', FileType::class, [ + 'label' => 'Problem statement', 'required' => false, 'attr' => [ 'accept' => 'text/html,text/plain,application/pdf', ], ]); - $builder->add('clearProblemtext', CheckboxType::class, [ - 'label' => 'Delete problem text', + $builder->add('clearProblemstatement', CheckboxType::class, [ + 'label' => 'Delete problem statement', 'required' => false, ]); $builder->add('runExecutable', EntityType::class, [ @@ -83,14 +83,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); $builder->add('save', SubmitType::class); - // Remove clearProblemtext field when we do not have a problem text. + // Remove clearProblemstatement field when we do not have a problem text. $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { /** @var Problem|null $problem */ $problem = $event->getData(); $form = $event->getForm(); - if ($problem && !$problem->getProblemtext()) { - $form->remove('clearProblemtext'); + if ($problem && !$problem->getProblemstatement()) { + $form->remove('clearProblemstatement'); } }); } diff --git a/webapp/src/Form/Type/TeamAffiliationType.php b/webapp/src/Form/Type/TeamAffiliationType.php index 6c4b8cc2914..46799a0dbd8 100644 --- a/webapp/src/Form/Type/TeamAffiliationType.php +++ b/webapp/src/Form/Type/TeamAffiliationType.php @@ -82,7 +82,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $affiliation = $event->getData(); $form = $event->getForm(); - $id = $affiliation?->getApiId($this->eventLogService); + $id = $affiliation?->getExternalid(); if (!$affiliation || !$this->dj->assetPath($id, 'affiliation')) { $form->remove('clearLogo'); diff --git a/webapp/src/Form/Type/UserRegistrationType.php b/webapp/src/Form/Type/UserRegistrationType.php index 194923811b7..66ac1b91e37 100644 --- a/webapp/src/Form/Type/UserRegistrationType.php +++ b/webapp/src/Form/Type/UserRegistrationType.php @@ -114,9 +114,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder ->add('affiliation', ChoiceType::class, [ 'choices' => [ - 'No affiliation' => 'none', - 'Add new affiliation' => 'new', 'Use existing affiliation' => 'existing', + 'Add new affiliation' => 'new', + 'No affiliation' => 'none', ], 'expanded' => true, 'mapped' => false, @@ -155,6 +155,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'mapped' => false, 'choice_label' => 'name', 'placeholder' => '-- Select affiliation --', + 'query_builder' => fn(EntityRepository $er) => $er + ->createQueryBuilder('a') + ->orderBy('a.name'), 'attr' => [ 'placeholder' => 'Affiliation', ], diff --git a/webapp/src/FosRestBundle/FlattenExceptionHandler.php b/webapp/src/FosRestBundle/FlattenExceptionHandler.php index 18889307815..d0fc918a78d 100644 --- a/webapp/src/FosRestBundle/FlattenExceptionHandler.php +++ b/webapp/src/FosRestBundle/FlattenExceptionHandler.php @@ -38,6 +38,11 @@ public static function getSubscribingMethods(): array ]; } + /** + * @param array{params: string[]} $type + * + * @return array{code: int, message: string} + */ public function serializeToJson( JsonSerializationVisitor $visitor, FlattenException $exception, diff --git a/webapp/src/Logger/VarargsLogMessageProcessor.php b/webapp/src/Logger/VarargsLogMessageProcessor.php index 787841be6dc..ba142c64b5e 100644 --- a/webapp/src/Logger/VarargsLogMessageProcessor.php +++ b/webapp/src/Logger/VarargsLogMessageProcessor.php @@ -2,7 +2,8 @@ /* * A simply message processor for Monolog, that uses printf style argument - * passing. + * passing. Only apply this if the message looks like a printf format string + * (aka contains at least one `%`) and the context is a plain list array. */ namespace App\Logger; @@ -17,7 +18,8 @@ class VarargsLogMessageProcessor implements ProcessorInterface { public function __invoke(LogRecord $record): LogRecord { - if (!str_contains($record->message, '%') || empty($record->context)) { + if (!str_contains($record->message, '%') || + empty($record->context) || !array_is_list($record->context)) { return $record; } diff --git a/webapp/src/NelmioApiDocBundle/ExternalDocDescriber.php b/webapp/src/NelmioApiDocBundle/ExternalDocDescriber.php new file mode 100644 index 00000000000..2805d51b55c --- /dev/null +++ b/webapp/src/NelmioApiDocBundle/ExternalDocDescriber.php @@ -0,0 +1,29 @@ +requestStack->getCurrentRequest(); + $this->decorated->describe($api); + Util::merge($api->servers[0], ['url' => $request->getSchemeAndHttpHost(),], true); + } +} diff --git a/webapp/src/Security/DOMJudgeBasicAuthenticator.php b/webapp/src/Security/DOMJudgeBasicAuthenticator.php index b39c329060b..a2947dbce37 100644 --- a/webapp/src/Security/DOMJudgeBasicAuthenticator.php +++ b/webapp/src/Security/DOMJudgeBasicAuthenticator.php @@ -67,7 +67,11 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio // Otherwise, we pass along to the next authenticator. if ($exception instanceof BadCredentialsException || $exception instanceof UserNotFoundException) { $resp = new Response('', Response::HTTP_UNAUTHORIZED); - $resp->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', 'Secured Area')); + + if (!$request->isXmlHttpRequest()) { + $resp->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', 'Secured Area')); + } + return $resp; } diff --git a/webapp/src/Serializer/ContestProblemVisitor.php b/webapp/src/Serializer/ContestProblemVisitor.php index 4951a29dcd3..e4ac6ca5db3 100644 --- a/webapp/src/Serializer/ContestProblemVisitor.php +++ b/webapp/src/Serializer/ContestProblemVisitor.php @@ -5,20 +5,14 @@ use App\DataTransferObject\FileWithName; use App\Entity\ContestProblem; use App\Service\DOMJudgeService; -use App\Service\EventLogService; -use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\JsonSerializationVisitor; -use JMS\Serializer\Metadata\StaticPropertyMetadata; class ContestProblemVisitor implements EventSubscriberInterface { - public function __construct( - protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService - ) {} + public function __construct(protected readonly DOMJudgeService $dj) {} /** * @return array @@ -43,12 +37,12 @@ public function onPreSerialize(ObjectEvent $event): void $contestProblem = $event->getObject(); // Problem statement - if ($contestProblem->getProblem()->getProblemtextType() === 'pdf') { + if ($contestProblem->getProblem()->getProblemstatementType() === 'pdf') { $route = $this->dj->apiRelativeUrl( 'v4_app_api_problem_statement', [ - 'cid' => $contestProblem->getContest()->getApiId($this->eventLogService), - 'id' => $contestProblem->getApiId($this->eventLogService), + 'cid' => $contestProblem->getContest()->getExternalid(), + 'id' => $contestProblem->getExternalId(), ] ); $contestProblem->getProblem()->setStatementForApi(new FileWithName( diff --git a/webapp/src/Serializer/ContestVisitor.php b/webapp/src/Serializer/ContestVisitor.php index 6614b1c7248..b39e3a7fae2 100644 --- a/webapp/src/Serializer/ContestVisitor.php +++ b/webapp/src/Serializer/ContestVisitor.php @@ -2,23 +2,23 @@ namespace App\Serializer; +use App\DataTransferObject\FileWithName; use App\DataTransferObject\ImageFile; use App\Entity\Contest; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\Metadata\StaticPropertyMetadata; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; class ContestVisitor implements EventSubscriberInterface { public function __construct( protected readonly ConfigurationService $config, protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService ) {} /** @@ -48,7 +48,7 @@ public function onPreSerialize(ObjectEvent $event): void ); $contest->setPenaltyTimeForApi((int)$this->config->get('penalty_time')); - $id = $contest->getApiId($this->eventLogService); + $id = $contest->getExternalid(); // Banner if ($banner = $this->dj->assetPath($id, 'contest', true)) { @@ -69,5 +69,32 @@ public function onPreSerialize(ObjectEvent $event): void } else { $contest->setBannerForApi(); } + + $hasAccess = $this->dj->checkrole('jury') || + $this->dj->checkrole('api_reader') || + $contest->getFreezeData()->started(); + + // Problem statement + if ($contest->getContestProblemsetType() && $hasAccess) { + $route = $this->dj->apiRelativeUrl( + 'v4_contest_problemset', + [ + 'cid' => $id, + ] + ); + $mimeType = match ($contest->getContestProblemsetType()) { + 'pdf' => 'application/pdf', + 'html' => 'text/html', + 'txt' => 'text/plain', + default => throw new BadRequestHttpException(sprintf('Contest c%d text has unknown type', $contest->getCid())), + }; + $contest->setProblemsetForApi(new FileWithName( + $route, + $mimeType, + 'problemset.' . $contest->getContestProblemsetType() + )); + } else { + $contest->setProblemsetForApi(); + } } } diff --git a/webapp/src/Serializer/SetExternalIdVisitor.php b/webapp/src/Serializer/SetExternalIdVisitor.php deleted file mode 100644 index 6b400993b94..00000000000 --- a/webapp/src/Serializer/SetExternalIdVisitor.php +++ /dev/null @@ -1,111 +0,0 @@ - - */ - public static function getSubscribedEvents(): array - { - return [ - [ - 'event' => Events::POST_SERIALIZE, - 'format' => 'json', - 'method' => 'onPostSerialize' - ], - ]; - } - - public function onPostSerialize(ObjectEvent $event): void - { - /** @var JsonSerializationVisitor $visitor */ - $visitor = $event->getVisitor(); - $object = $event->getObject(); - - try { - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity($object::class)) { - $method = sprintf('get%s', ucfirst($externalIdField)); - if (method_exists($object, $method)) { - $property = new StaticPropertyMetadata( - $object::class, - 'id', - null - ); - $visitor->visitProperty($property, $object->{$method}()); - } - } elseif (($object instanceof Submission || $object instanceof Clarification) && $object->getExternalid() !== null) { - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID - $property = new StaticPropertyMetadata( - $object::class, - 'id', - null - ); - $visitor->visitProperty($property, $object->getExternalid()); - } - } catch (BadMethodCallException) { - // Ignore these exceptions, as this means this is not an entity or it is not configured. - } - - if ($object instanceof ExternalRelationshipEntityInterface) { - foreach ($object->getExternalRelationships() as $field => $entity) { - try { - if (is_array($entity)) { - if (empty($entity) || !($externalIdField = $this->eventLogService->externalIdFieldForEntity($entity[0]::class))) { - continue; - } - $method = sprintf('get%s', ucfirst($externalIdField)); - $property = new StaticPropertyMetadata( - $object::class, - $field, - null - ); - $data = []; - foreach ($entity as $item) { - $data[] = $item->{$method}(); - } - $visitor->visitProperty($property, $data); - } elseif ($entity && $externalIdField = $this->eventLogService->externalIdFieldForEntity($entity::class)) { - $method = sprintf('get%s', ucfirst($externalIdField)); - if (method_exists($entity, $method)) { - $property = new StaticPropertyMetadata( - $object::class, - $field, - null - ); - $visitor->visitProperty($property, $entity->{$method}()); - } - } elseif ($entity && ($entity instanceof Submission || $entity instanceof Clarification) && $entity->getExternalid() !== null) { - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID - $property = new StaticPropertyMetadata( - $entity::class, - $field, - null - ); - $visitor->visitProperty($property, $entity->getExternalid()); - } - } catch (BadMethodCallException) { - // Ignore these exceptions, as this means this is not an entity or it is not configured. - } - } - } - } -} diff --git a/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php b/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php index 191db015839..c95ac90a1dd 100644 --- a/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php +++ b/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php @@ -28,7 +28,7 @@ public function denormalize( string $type, ?string $format = null, array $context = [] - ) { + ): mixed { if (!$this->supportsDenormalization($data, $type, $format, $context)) { throw new InvalidArgumentException('Unsupported data.'); } diff --git a/webapp/src/Serializer/SubmissionVisitor.php b/webapp/src/Serializer/SubmissionVisitor.php index 45d45b8ea98..d85bb1d33bc 100644 --- a/webapp/src/Serializer/SubmissionVisitor.php +++ b/webapp/src/Serializer/SubmissionVisitor.php @@ -6,20 +6,17 @@ use App\DataTransferObject\FileWithName; use App\Entity\Submission; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; -use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\Metadata\StaticPropertyMetadata; class SubmissionVisitor implements EventSubscriberInterface { public function __construct( protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService, - protected readonly EntityManagerInterface $em + protected readonly EntityManagerInterface $em, ) {} /** @@ -45,8 +42,8 @@ public function onPreSerialize(ObjectEvent $event): void $route = $this->dj->apiRelativeUrl( 'v4_submission_files', [ - 'cid' => $submission->getContest()->getApiId($this->eventLogService), - 'id' => $submission->getExternalid() ?? $submission->getSubmitid(), + 'cid' => $submission->getContest()->getExternalid(), + 'id' => $submission->getExternalid(), ] ); $property = new StaticPropertyMetadata( diff --git a/webapp/src/Serializer/TeamAffiliationVisitor.php b/webapp/src/Serializer/TeamAffiliationVisitor.php index 98fe4ab5364..3a65c8b37eb 100644 --- a/webapp/src/Serializer/TeamAffiliationVisitor.php +++ b/webapp/src/Serializer/TeamAffiliationVisitor.php @@ -6,7 +6,6 @@ use App\Entity\TeamAffiliation; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; @@ -18,8 +17,7 @@ class TeamAffiliationVisitor implements EventSubscriberInterface public function __construct( protected readonly DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLogService, - protected readonly RequestStack $requestStack + protected readonly RequestStack $requestStack, ) {} /** @@ -42,7 +40,7 @@ public function onPreSerialize(ObjectEvent $event): void /** @var TeamAffiliation $affiliation */ $affiliation = $event->getObject(); - $id = $affiliation->getApiId($this->eventLogService); + $id = $affiliation->getExternalid(); // Country flag if ($this->config->get('show_flags') && $affiliation->getCountry()) { diff --git a/webapp/src/Serializer/TeamVisitor.php b/webapp/src/Serializer/TeamVisitor.php index 2b8edf0a5fe..45b61e0f738 100644 --- a/webapp/src/Serializer/TeamVisitor.php +++ b/webapp/src/Serializer/TeamVisitor.php @@ -5,7 +5,6 @@ use App\DataTransferObject\ImageFile; use App\Entity\Team; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; @@ -18,7 +17,6 @@ class TeamVisitor implements EventSubscriberInterface { public function __construct( protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService, protected readonly RequestStack $requestStack ) {} @@ -57,7 +55,7 @@ public function onPostSerialize(ObjectEvent $event): void 'label', null ); - $visitor->visitProperty($property, $team->getApiId($this->eventLogService)); + $visitor->visitProperty($property, $team->getExternalid()); } } @@ -66,7 +64,7 @@ public function onPreSerialize(ObjectEvent $event): void /** @var Team $team */ $team = $event->getObject(); - $id = $team->getApiId($this->eventLogService); + $id = $team->getExternalid(); // Check if the asset actually exists if (!($teamPhoto = $this->dj->assetPath($id, 'team', true))) { diff --git a/webapp/src/Service/AssetUpdateService.php b/webapp/src/Service/AssetUpdateService.php index 2866b1ea2d9..feb6936e353 100644 --- a/webapp/src/Service/AssetUpdateService.php +++ b/webapp/src/Service/AssetUpdateService.php @@ -21,7 +21,7 @@ public function updateAssets(AssetEntityInterface &$entity): void foreach ($entity->getAssetProperties() as $assetProperty) { $assetPaths = []; foreach (DOMJudgeService::MIMETYPE_TO_EXTENSION as $mimetype => $extension) { - $assetPaths[$mimetype] = $this->dj->fullAssetPath($entity, $assetProperty, $this->eventLog->externalIdFieldForEntity($entity) !== null, $extension); + $assetPaths[$mimetype] = $this->dj->fullAssetPath($entity, $assetProperty, $extension); } if ($entity->isClearAsset($assetProperty)) { foreach ($assetPaths as $assetPath) { diff --git a/webapp/src/Service/AwardService.php b/webapp/src/Service/AwardService.php index c01acdabe10..0631d00538c 100644 --- a/webapp/src/Service/AwardService.php +++ b/webapp/src/Service/AwardService.php @@ -12,24 +12,20 @@ class AwardService /** @var array */ protected array $awardCache = []; - public function __construct(protected readonly EventLogService $eventLogService) - { - } - protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void { $group_winners = $problem_winners = $problem_shortname = []; $groups = []; foreach ($scoreboard->getTeams() as $team) { - $teamid = $team->getApiId($this->eventLogService); + $teamid = $team->getExternalid(); if ($scoreboard->isBestInCategory($team)) { - $catId = $team->getCategory()->getApiId($this->eventLogService); + $catId = $team->getCategory()->getExternalid(); $group_winners[$catId][] = $teamid; $groups[$catId] = $team->getCategory()->getName(); } foreach ($scoreboard->getProblems() as $problem) { $shortname = $problem->getShortname(); - $probid = $problem->getApiId($this->eventLogService); + $probid = $problem->getExternalId(); if ($scoreboard->solvedFirst($team, $problem)) { $problem_winners[$probid][] = $teamid; $problem_shortname[$probid] = $shortname; @@ -56,6 +52,10 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void $overall_winners = $medal_winners = []; $additionalBronzeMedals = $contest->getB() ?? 0; + // Do not consider additional bronze medals until the contest is unfrozen. + if (!$scoreboard->hasRestrictedAccess()) { + $additionalBronzeMedals = 0; + } $currentSortOrder = -1; @@ -74,7 +74,7 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void continue; } $rank = $teamScore->rank; - $teamid = $teamScore->team->getApiId($this->eventLogService); + $teamid = $teamScore->team->getExternalid(); if ($rank === 1) { $overall_winners[] = $teamid; } @@ -141,7 +141,7 @@ public function getAward(Contest $contest, Scoreboard $scoreboard, string $reque public function medalType(Team $team, Contest $contest, Scoreboard $scoreboard): ?string { - $teamid = $team->getApiId($this->eventLogService); + $teamid = $team->getExternalid(); if (!isset($this->awardCache[$contest->getCid()])) { $this->loadAwards($contest, $scoreboard); } diff --git a/webapp/src/Service/BalloonService.php b/webapp/src/Service/BalloonService.php index 38bfcbca8b6..cf16a984846 100644 --- a/webapp/src/Service/BalloonService.php +++ b/webapp/src/Service/BalloonService.php @@ -6,10 +6,12 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Judging; +use App\Entity\Problem; use App\Entity\ScoreCache; use App\Entity\Submission; use App\Entity\Team; use App\Entity\TeamAffiliation; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; @@ -36,7 +38,7 @@ public function __construct( public function updateBalloons( Contest $contest, Submission $submission, - ?Judging $judging = null + Judging $judging ): void { // Balloon processing disabled for contest. if (!$contest->getProcessBalloons()) { @@ -44,7 +46,7 @@ public function updateBalloons( } // Make sure judging is correct. - if (!$judging || $judging->getResult() !== Judging::RESULT_CORRECT) { + if ($judging->getResult() !== Judging::RESULT_CORRECT) { return; } @@ -57,12 +59,10 @@ public function updateBalloons( // Prevent duplicate balloons in case of multiple correct submissions. $numCorrect = $this->em->createQueryBuilder() ->from(Balloon::class, 'b') - ->join('b.submission', 's') ->select('COUNT(b.submission) AS numBalloons') - ->andWhere('s.valid = 1') - ->andWhere('s.problem = :probid') - ->andWhere('s.team = :teamid') - ->andWhere('s.contest = :cid') + ->andWhere('b.problem = :probid') + ->andWhere('b.team = :teamid') + ->andWhere('b.contest = :cid') ->setParameter('probid', $submission->getProblem()) ->setParameter('teamid', $submission->getTeam()) ->setParameter('cid', $submission->getContest()) @@ -71,10 +71,16 @@ public function updateBalloons( if ($numCorrect == 0) { $balloon = new Balloon(); - $balloon->setSubmission( - $this->em->getReference(Submission::class, $submission->getSubmitid())); + $balloon->setSubmission($this->em->getReference(Submission::class, $submission->getSubmitid())); + $balloon->setTeam($this->em->getReference(Team::class, $submission->getTeamId())); + $balloon->setContest( + $this->em->getReference(Contest::class, $submission->getContest()->getCid())); + $balloon->setProblem($this->em->getReference(Problem::class, $submission->getProblemId())); $this->em->persist($balloon); - $this->em->flush(); + try { + $this->em->flush(); + } catch (UniqueConstraintViolationException $e) { + } } } @@ -108,10 +114,10 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array 'a.affilid AS affilid', 'a.shortname AS affilshort') ->from(Balloon::class, 'b') ->leftJoin('b.submission', 's') - ->leftJoin('s.problem', 'p') - ->leftJoin('s.contest', 'co') + ->leftJoin('b.problem', 'p') + ->leftJoin('b.contest', 'co') ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'co.cid = cp.contest AND p.probid = cp.problem') - ->leftJoin('s.team', 't') + ->leftJoin('b.team', 't') ->leftJoin('t.category', 'c') ->leftJoin('t.affiliation', 'a') ->andWhere('co.cid = :cid') diff --git a/webapp/src/Service/CheckConfigService.php b/webapp/src/Service/CheckConfigService.php index db70509ac02..4fa00537c22 100644 --- a/webapp/src/Service/CheckConfigService.php +++ b/webapp/src/Service/CheckConfigService.php @@ -5,6 +5,7 @@ use App\DataTransferObject\ConfigCheckItem; use App\Entity\ContestProblem; use App\Entity\Executable; +use App\Entity\HasExternalIdInterface; use App\Entity\Language; use App\Entity\Problem; use App\Entity\Team; @@ -485,7 +486,7 @@ public function checkContestBanners(): ConfigCheckItem $desc = ''; $result = 'O'; foreach ($contests as $contest) { - if ($cid = $contest->getApiId($this->eventLogService)) { + if ($cid = $contest->getExternalid()) { $bannerpath = $this->dj->assetPath($cid, 'contest', true); $contestName = 'c' . $contest->getCid() . ' (' . $contest->getShortname() . ')'; if ($bannerpath) { @@ -585,6 +586,16 @@ public function checkProblemsValidate(): ConfigCheckItem } } } + + foreach ($problem->getContestProblems() as $contestProblem) { + if (!$contestProblem->getAllowJudge()) { + $result = 'E'; + $moreproblemerrors[$probid] .= sprintf( + "p%s is disabled in contest '%s'\n", + $probid, $contestProblem->getContest()->getName() + ); + } + } } $desc = ''; @@ -638,6 +649,11 @@ public function checkLanguagesValidate(): ConfigCheckItem $morelanguageerrors[$langid] .= sprintf("Compile script %s exists but is of wrong type (%s instead of compile) for %s\n", $compile, $exec->getType(), $langid); } } + + if ($language->getAllowSubmit() && !$language->getAllowJudge()) { + $result = 'E'; + $morelanguageerrors[$langid] .= sprintf("Language '%s' is allowed to be submit, but not judged.\n", $langid); + } } $desc = ''; @@ -670,7 +686,7 @@ public function checkTeamPhotos(): ConfigCheckItem $desc = ''; $result = 'O'; foreach ($teams as $team) { - if ($tid = $team->getApiId($this->eventLogService)) { + if ($tid = $team->getExternalid()) { $photopath = $this->dj->assetPath($tid, 'team', true); if ($photopath && ($filesize = filesize($photopath)) > 5 * 1024 * 1024) { $result = 'W'; @@ -714,7 +730,7 @@ public function checkAffiliations(): ConfigCheckItem continue; } - if ($aid = $affiliation->getApiId($this->eventLogService)) { + if ($aid = $affiliation->getExternalid()) { $logopath = $this->dj->assetPath($aid, 'affiliation', true); $logopathMask = str_replace('.jpg', '.{jpg,png,svg}', $this->dj->assetPath($aid, 'affiliation', true, 'jpg')); if (!$logopath) { @@ -735,7 +751,7 @@ public function checkAffiliations(): ConfigCheckItem } elseif ($width !== 64 || $height !== 64) { // For other images we check the size $result = 'W'; - $desc .= sprintf("Logo for %s is not 64x64\n", $affiliation->getShortname()); + $desc .= sprintf("Logo for %s is not 64x64 but %dx%d\n", $affiliation->getShortname(), $width, $height); } } } @@ -830,10 +846,8 @@ public function checkAllExternalIdentifiers(): array $class = sprintf('App\\Entity\\%s', $shortClass); try { if (class_exists($class) - // ContestProblem is checked using Problem. - && $class != ContestProblem::class - && ($externalIdField = $this->eventLogService->externalIdFieldForEntity($class))) { - $result[$shortClass] = $this->checkExternalIdentifiers($class, $externalIdField); + && is_a($class, HasExternalIdInterface::class, true)) { + $result[$shortClass] = $this->checkExternalIdentifiers($class); } } catch (BadMethodCallException) { // Ignore, this entity does not have an API endpoint. @@ -847,7 +861,7 @@ public function checkAllExternalIdentifiers(): array /** * @param class-string $class */ - protected function checkExternalIdentifiers(string $class, string $externalIdField): ConfigCheckItem + protected function checkExternalIdentifiers(string $class): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $parts = explode('\\', $class); @@ -857,7 +871,7 @@ protected function checkExternalIdentifiers(string $class, string $externalIdFie $rowsWithoutExternalId = $this->em->createQueryBuilder() ->from($class, 'e') ->select('e') - ->andWhere(sprintf('e.%s IS NULL or e.%s = :empty', $externalIdField, $externalIdField)) + ->andWhere('e.externalid IS NULL or e.externalid = :empty') ->setParameter('empty', '') ->getQuery() ->getResult(); diff --git a/webapp/src/Service/Compare/AbstractCompareService.php b/webapp/src/Service/Compare/AbstractCompareService.php new file mode 100644 index 00000000000..783bacba21d --- /dev/null +++ b/webapp/src/Service/Compare/AbstractCompareService.php @@ -0,0 +1,84 @@ +addMessage(MessageType::ERROR, sprintf('File "%s" does not exist', $file1)); + $success = false; + } + if (!file_exists($file2)) { + $this->addMessage(MessageType::ERROR, sprintf('File "%s" does not exist', $file2)); + $success = false; + } + if (!$success) { + return $this->messages; + } + + try { + $object1 = $this->parseFile($file1); + } catch (ExceptionInterface $e) { + $this->addMessage(MessageType::ERROR, sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); + } + try { + $object2 = $this->parseFile($file2); + } catch (ExceptionInterface $e) { + $this->addMessage(MessageType::ERROR, sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); + } + + if (!isset($object1) || !isset($object2)) { + return $this->messages; + } + + $this->compare($object1, $object2); + + return $this->messages; + } + + /** + * @return T|null + * @throws ExceptionInterface + */ + abstract protected function parseFile(string $file); + + /** + * @param T $object1 + * @param T $object2 + */ + abstract public function compare($object1, $object2): void; + + protected function addMessage( + MessageType $type, + string $message, + ?string $source = null, + ?string $target = null, + ): void { + $this->messages[] = new Message($type, $message, $source, $target); + } + + /** + * @return Message[] + */ + public function getMessages(): array + { + return $this->messages; + } +} diff --git a/webapp/src/Service/Compare/AwardCompareService.php b/webapp/src/Service/Compare/AwardCompareService.php new file mode 100644 index 00000000000..961de608c75 --- /dev/null +++ b/webapp/src/Service/Compare/AwardCompareService.php @@ -0,0 +1,61 @@ + + */ +class AwardCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + return $this->serializer->deserialize(file_get_contents($file), Award::class . '[]', 'json'); + } + + public function compare($object1, $object2): void + { + $awards1Indexed = []; + foreach ($object1 as $award) { + $awards1Indexed[$award->id] = $award; + } + + $awards2Indexed = []; + foreach ($object2 as $award) { + $awards2Indexed[$award->id] = $award; + } + + foreach ($awards1Indexed as $awardId => $award) { + if (!isset($awards2Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage(MessageType::INFO, sprintf('Award "%s" not found in second file, but has no team ID\'s in first file', $awardId)); + } else { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" not found in second file', $awardId)); + } + } else { + $award2 = $awards2Indexed[$awardId]; + if ($award->citation !== $award2->citation) { + $this->addMessage(MessageType::WARNING, sprintf('Award "%s" has different citation', $awardId), $award->citation, $award2->citation); + } + $award1TeamIds = $award->teamIds; + sort($award1TeamIds); + $award2TeamIds = $award2->teamIds; + sort($award2TeamIds); + if ($award1TeamIds !== $award2TeamIds) { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" has different team ID\'s', $awardId), implode(', ', $award->teamIds), implode(', ', $award2->teamIds)); + } + } + } + + foreach ($awards2Indexed as $awardId => $award) { + if (!isset($awards1Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage(MessageType::INFO, sprintf('Award "%s" not found in first file, but has no team ID\'s in second file', $awardId)); + } else { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" not found in first file', $awardId)); + } + } + } + } +} diff --git a/webapp/src/Service/Compare/Message.php b/webapp/src/Service/Compare/Message.php new file mode 100644 index 00000000000..cd198a34365 --- /dev/null +++ b/webapp/src/Service/Compare/Message.php @@ -0,0 +1,13 @@ + + */ +class ResultsCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + $resultsContents = file_get_contents($file); + if (!str_starts_with($resultsContents, "results\t1")) { + $this->addMessage(MessageType::ERROR, sprintf("File \"%s\" does not start with \"results\t1\"", $file)); + return null; + } + + $resultsContents = substr($resultsContents, strpos($resultsContents, "\n") + 1); + + // Prefix file with a fake header, so we can deserialize them + $resultsContents = "team_id\trank\taward\tnum_solved\ttotal_time\ttime_of_last_submission\tgroup_winner\n" . $resultsContents; + + $results = $this->serializer->deserialize($resultsContents, ResultRow::class . '[]', 'csv', [ + CsvEncoder::DELIMITER_KEY => "\t", + ]); + + // Sort results: first by num_solved, then by total_time + usort($results, fn( + ResultRow $a, + ResultRow $b + ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); + + return $results; + } + + public function compare($object1, $object2): void + { + /** @var array $results1Indexed */ + $results1Indexed = []; + foreach ($object1 as $result) { + $results1Indexed[$result->teamId] = $result; + } + + /** @var array $results2Indexed */ + $results2Indexed = []; + foreach ($object2 as $result) { + $results2Indexed[$result->teamId] = $result; + } + + foreach ($object1 as $result) { + if (!isset($results2Indexed[$result->teamId])) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" not found in second file', $result->teamId)); + } else { + $result2 = $results2Indexed[$result->teamId]; + if ($result->rank !== $result2->rank) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank); + } + if ($result->award !== $result2->award) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different award', $result->teamId), $result->award, $result2->award); + } + if ($result->numSolved !== $result2->numSolved) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different num solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved); + } + if ($result->totalTime !== $result2->totalTime) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different total time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); + } + if ($result->timeOfLastSubmission !== $result2->timeOfLastSubmission) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different last time', $result->teamId), (string)$result->timeOfLastSubmission, (string)$result2->timeOfLastSubmission); + } + if ($result->groupWinner !== $result2->groupWinner) { + $this->addMessage(MessageType::WARNING, sprintf('Team "%s" has different group winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); + } + } + } + + foreach ($object2 as $result) { + if (!isset($results1Indexed[$result->teamId])) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" not found in first file', $result->teamId)); + } + } + } +} diff --git a/webapp/src/Service/Compare/ScoreboardCompareService.php b/webapp/src/Service/Compare/ScoreboardCompareService.php new file mode 100644 index 00000000000..1f05941ae9f --- /dev/null +++ b/webapp/src/Service/Compare/ScoreboardCompareService.php @@ -0,0 +1,134 @@ + + */ +class ScoreboardCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + return $this->serializer->deserialize(file_get_contents($file), Scoreboard::class, 'json', ['disable_type_enforcement' => true]); + } + + public function compare($object1, $object2): void + { + if ($object1->eventId !== $object2->eventId) { + $this->addMessage(MessageType::INFO, 'Event ID does not match', $object1->eventId, $object2->eventId); + } + + if ($object1->time !== $object2->time) { + $this->addMessage(MessageType::INFO, 'Time does not match', $object1->time, $object2->time); + } + + if ($object1->contestTime !== $object2->contestTime) { + $this->addMessage(MessageType::INFO, 'Contest time does not match', $object1->contestTime, $object2->contestTime); + } + + if (($object1->state->started ?? '') !== ($object2->state->started ?? '')) { + $this->addMessage(MessageType::WARNING, 'State started does not match', $object1->state->started, $object2->state->started); + } + + if (($object1->state->ended ?? '') !== ($object2->state->ended ?? '')) { + $this->addMessage(MessageType::WARNING, 'State ended does not match', $object1->state->ended, $object2->state->ended); + } + + if (($object1->state->frozen ?? '') !== ($object2->state->frozen ?? '')) { + $this->addMessage(MessageType::WARNING, 'State frozen does not match', $object1->state->frozen, $object2->state->frozen); + } + + if (($object1->state->thawed ?? '') !== ($object2->state->thawed ?? '')) { + $this->addMessage(MessageType::WARNING, 'State thawed does not match', $object1->state->thawed, $object2->state->thawed); + } + + if (($object1->state->finalized ?? '') !== ($object2->state->finalized ?? '')) { + $this->addMessage(MessageType::WARNING, 'State finalized does not match', $object1->state->finalized, $object2->state->finalized); + } + + if (($object1->state->endOfUpdates ?? '') !== ($object2->state->endOfUpdates ?? '')) { + $this->addMessage(MessageType::WARNING, 'State end of updates does not match', $object1->state->endOfUpdates, $object2->state->endOfUpdates); + } + + if (count($object1->rows) !== count($object2->rows)) { + $this->addMessage(MessageType::ERROR, 'Number of rows does not match', (string)count($object1->rows), (string)count($object2->rows)); + } + + foreach ($object1->rows as $index => $row) { + if ($row->teamId !== $object2->rows[$index]->teamId) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: team ID does not match', $index), $row->teamId, $object2->rows[$index]->teamId); + } + + if ($row->rank !== $object2->rows[$index]->rank) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: rank does not match', $index), (string)$row->rank, (string)$object2->rows[$index]->rank); + } + + if ($row->score->numSolved !== $object2->rows[$index]->score->numSolved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: num solved does not match', $index), (string)$row->score->numSolved, (string)$object2->rows[$index]->score->numSolved); + } + + if ($row->score->totalTime !== $object2->rows[$index]->score->totalTime) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: total time does not match', $index), (string)$row->score->totalTime, (string)$object2->rows[$index]->score->totalTime); + } + + foreach ($row->problems as $problem) { + /** @var Problem|null $problemForSecond */ + $problemForSecond = null; + + foreach ($object2->rows[$index]->problems as $problem2) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForSecond = $problem2; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForSecond = $problem2; + break; + } + } + + if ($problemForSecond === null && $problem->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in first file, but not found in second file', $index, $problem->problemId)); + } elseif ($problemForSecond !== null && $problem->solved !== $problemForSecond->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved does not match', $index, $problem->problemId), (string)$problem->solved, (string)$problemForSecond->solved); + } + + if ($problemForSecond) { + if ($problem->numJudged !== $problemForSecond->numJudged) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s num judged does not match', $index, $problem->problemId), (string)$problem->numJudged, (string)$problemForSecond->numJudged); + } + + if ($problem->numPending !== $problemForSecond->numPending) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s num pending does not match', $index, $problem->problemId), (string)$problem->numPending, (string)$problemForSecond->numPending); + } + + if ($problem->time !== $problemForSecond->time) { + // This is an info message for now, since PC^2 doesn't expose time info + $this->addMessage(MessageType::INFO, sprintf('Row %d: Problem %s time does not match', $index, $problem->problemId), (string)$problem->time, (string)$problemForSecond->time); + } + } + } + + foreach ($object2->rows[$index]->problems as $problem2) { + $problemForFirst = null; + + foreach ($row->problems as $problem) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForFirst = $problem; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForFirst = $problem; + break; + } + } + + if ($problemForFirst === null && $problem2->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in second file, but not found in first file', $index, $problem2->problemId)); + } + } + } + } +} diff --git a/webapp/src/Service/ConfigurationService.php b/webapp/src/Service/ConfigurationService.php index 65457f168a0..6fe72c340f3 100644 --- a/webapp/src/Service/ConfigurationService.php +++ b/webapp/src/Service/ConfigurationService.php @@ -70,6 +70,11 @@ public function get(string $name, bool $onlyIfPublic = false) return $value; } + public function getCategory(string $name): string + { + return $this->getConfigSpecification()[$name]->category; + } + /** * Get all the configuration values, indexed by name. * @@ -161,6 +166,7 @@ public function saveChanges( array $dataToSet, EventLogService $eventLog, DOMJudgeService $dj, + bool $treatMissingBooleansAsFalse = true, ?array &$options = null ): array { $specs = $this->getConfigSpecification(); @@ -191,7 +197,7 @@ public function saveChanges( $options[$specName] = $optionToSet; } if (!array_key_exists($specName, $dataToSet)) { - if ($spec->type == 'bool') { + if ($spec->type == 'bool' && $treatMissingBooleansAsFalse) { // Special-case bool, since checkboxes don't return a // value when unset. $val = false; diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 08a1a2430fe..97cf50dedf1 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -67,9 +67,6 @@ class DOMJudgeService protected ?Executable $defaultCompareExecutable = null; protected ?Executable $defaultRunExecutable = null; - final public const DATA_SOURCE_LOCAL = 0; - final public const DATA_SOURCE_CONFIGURATION_EXTERNAL = 1; - final public const DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL = 2; final public const EVAL_DEFAULT = 0; final public const EVAL_LAZY = 1; final public const EVAL_FULL = 2; @@ -99,7 +96,7 @@ public function __construct( protected readonly Environment $twig, #[Autowire('%kernel.project_dir%')] protected string $projectDir, - #[Autowire('%domjudge.libvendordir%')] + #[Autowire('%domjudge.vendordir%')] protected string $vendorDir, ) {} @@ -385,7 +382,7 @@ public function getUpdates(): array ->setParameter('status', 'open') ->getQuery()->getResult(); - if ($this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL) { + if ($this->shadowMode()) { if ($contest) { $shadow_difference_count = $this->em->createQueryBuilder() ->from(Submission::class, 's') @@ -422,18 +419,18 @@ public function getUpdates(): array } } - if ($this->checkrole('balloon')) { + if ($this->checkrole('balloon') && $contest) { $balloonsQuery = $this->em->createQueryBuilder() - ->select('b.balloonid', 't.name', 't.location', 'p.name AS pname') - ->from(Balloon::class, 'b') - ->leftJoin('b.submission', 's') - ->leftJoin('s.problem', 'p') - ->leftJoin('s.contest', 'co') - ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'co.cid = cp.contest AND p.probid = cp.problem') - ->leftJoin('s.team', 't') - ->andWhere('co.cid = :cid') - ->andWhere('b.done = 0') - ->setParameter('cid', $contest->getCid()); + ->select('b.balloonid', 't.name', 't.location', 'p.name AS pname') + ->from(Balloon::class, 'b') + ->leftJoin('b.submission', 's') + ->leftJoin('s.problem', 'p') + ->leftJoin('s.contest', 'co') + ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'co.cid = cp.contest AND p.probid = cp.problem') + ->leftJoin('s.team', 't') + ->andWhere('co.cid = :cid') + ->andWhere('b.done = 0') + ->setParameter('cid', $contest->getCid()); $freezetime = $contest->getFreezeTime(); if ($freezetime !== null && !(bool)$this->config->get('show_balloons_postfreeze')) { @@ -710,14 +707,18 @@ public function getCacheDir(): string public function openZipFile(string $filename): ZipArchive { $zip = new ZipArchive(); - $res = $zip->open($filename, ZIPARCHIVE::CHECKCONS); - if ($res === ZIPARCHIVE::ER_NOZIP || $res === ZIPARCHIVE::ER_INCONS) { - throw new BadRequestHttpException('No valid zip archive given'); + $res = $zip->open($filename, ZipArchive::CHECKCONS); + if ($res === ZipArchive::ER_NOZIP) { + throw new BadRequestHttpException('No valid ZIP archive given.'); + } elseif ($res === ZipArchive::ER_INCONS) { + throw new BadRequestHttpException( + 'ZIP archive is inconsistent; this can happen when using built-in graphical ZIP tools on Mac OS or Ubuntu,' + . ' use the command line zip tool instead, e.g.: zip -r ../problemarchive.zip *'); } elseif ($res === ZIPARCHIVE::ER_MEMORY) { - throw new ServiceUnavailableHttpException(null, 'Not enough memory to extract zip archive'); + throw new ServiceUnavailableHttpException(null, 'Not enough memory to extract ZIP archive.'); } elseif ($res !== true) { throw new ServiceUnavailableHttpException(null, - 'Unknown error while extracting zip archive: ' . print_r($res, true)); + 'Unknown error while extracting ZIP archive: ' . print_r($res, true)); } return $zip; @@ -861,7 +862,7 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse ->innerJoin('c.problems', 'cp') ->innerJoin('cp.problem', 'p') ->leftJoin('p.attachments', 'a') - ->leftJoin('p.problemTextContent', 'content') + ->leftJoin('p.problemStatementContent', 'content') ->select('c', 'cp', 'p', 'a', 'content') ->andWhere('c.cid = :cid') ->setParameter('cid', $contest->getCid()) @@ -882,9 +883,9 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse foreach ($contest->getProblems() as $problem) { $this->addSamplesToZip($zip, $problem, $problem->getShortname()); - if ($problem->getProblem()->getProblemtextType()) { - $filename = sprintf('%s/statement.%s', $problem->getShortname(), $problem->getProblem()->getProblemtextType()); - $zip->addFromString($filename, $problem->getProblem()->getProblemtext()); + if ($problem->getProblem()->getProblemstatementType()) { + $filename = sprintf('%s/statement.%s', $problem->getShortname(), $problem->getProblem()->getProblemstatementType()); + $zip->addFromString($filename, $problem->getProblem()->getProblemstatement()); } /** @var ProblemAttachment $attachment */ @@ -894,6 +895,11 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse } } + if ($contest->getContestProblemsetType()) { + $filename = sprintf('contest.%s', $contest->getContestProblemsetType()); + $zip->addFromString($filename, $contest->getContestProblemset()); + } + $zip->close(); $zipFileContents = file_get_contents($tempFilename); unlink($tempFilename); @@ -1160,111 +1166,11 @@ public function maybeCreateJudgeTasks(Judging $judging, int $priority = JudgeTas if ($submission->isImportError()) { return; } - - $evalOnDemand = false; - // We have 2 cases, the problem picks the global value or the value is set. - if ($problem->determineOnDemand($this->config->get('lazy_eval_results'))) { - $evalOnDemand = true; - - // Special case, we're shadow and someone submits on our side in that case - // we're not super lazy. - if ($this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL - && $submission->getExternalid() === null) { - $evalOnDemand = false; - } - if ($manualRequest) { - // When explicitly requested, judge the submission. - $evalOnDemand = false; - } - } - if (!$problem->getAllowJudge() || !$language->getAllowJudge() || $evalOnDemand) { + if (!$this->allowJudge($problem, $submission, $language, $manualRequest)) { return; } - // We use a mass insert query, since that is way faster than doing a separate insert for each testcase. - // We first insert judgetasks, then select their ID's and finally insert the judging runs. - - // Step 1: Create the template for the judgetasks. - $compileExecutable = $submission->getLanguage()->getCompileExecutable()->getImmutableExecutable(); - $judgetaskInsertParams = [ - ':type' => JudgeTaskType::JUDGING_RUN, - ':submitid' => $submission->getSubmitid(), - ':priority' => $priority, - ':jobid' => $judging->getJudgingid(), - ':uuid' => $judging->getUuid(), - ':compile_script_id' => $compileExecutable->getImmutableExecId(), - ':compare_script_id' => $this->getImmutableCompareExecutable($problem)->getImmutableExecId(), - ':run_script_id' => $this->getImmutableRunExecutable($problem)->getImmutableExecId(), - ':compile_config' => $this->getCompileConfig($submission), - ':run_config' => $this->getRunConfig($problem, $submission), - ':compare_config' => $this->getCompareConfig($problem), - ]; - - $judgetaskDefaultParamNames = array_keys($judgetaskInsertParams); - - // Step 2: Create and insert the judgetasks. - - $testcases = $problem->getProblem()->getTestcases(); - if (count($testcases) < 1) { - throw new BadRequestHttpException("No testcases set for problem {$problem->getProbid()}"); - } - $judgetaskInsertParts = []; - /** @var Testcase $testcase */ - foreach ($testcases as $testcase) { - $judgetaskInsertParts[] = sprintf( - '(%s, :testcase_id%d, :testcase_hash%d)', - implode(', ', $judgetaskDefaultParamNames), - $testcase->getTestcaseid(), - $testcase->getTestcaseid() - ); - $judgetaskInsertParams[':testcase_id' . $testcase->getTestcaseid()] = $testcase->getTestcaseid(); - $judgetaskInsertParams[':testcase_hash' . $testcase->getTestcaseid()] = $testcase->getMd5sumInput() . '_' . $testcase->getMd5sumOutput(); - } - $judgetaskColumns = array_map(fn(string $column) => substr($column, 1), $judgetaskDefaultParamNames); - $judgetaskInsertQuery = sprintf( - 'INSERT INTO judgetask (%s, testcase_id, testcase_hash) VALUES %s', - implode(', ', $judgetaskColumns), - implode(', ', $judgetaskInsertParts) - ); - - $judgetaskInsertParamsWithoutColon = []; - foreach ($judgetaskInsertParams as $key => $param) { - $key = str_replace(':', '', $key); - $judgetaskInsertParamsWithoutColon[$key] = $param; - } - - $this->em->getConnection()->executeQuery($judgetaskInsertQuery, $judgetaskInsertParamsWithoutColon); - - // Step 3: Fetch the judgetasks ID's per testcase. - $judgetaskData = $this->em->getConnection()->executeQuery( - 'SELECT judgetaskid, testcase_id FROM judgetask WHERE jobid = :jobid ORDER BY judgetaskid', - ['jobid' => $judging->getJudgingid()] - )->fetchAllAssociative(); - - // Step 4: Create and insert the corresponding judging runs. - $judgingRunInsertParams = [':judgingid' => $judging->getJudgingid()]; - $judgingRunInsertParts = []; - foreach ($judgetaskData as $judgetaskItem) { - $judgingRunInsertParts[] = sprintf( - '(:judgingid, :testcaseid%d, :judgetaskid%d)', - $judgetaskItem['judgetaskid'], - $judgetaskItem['judgetaskid'] - ); - $judgingRunInsertParams[':testcaseid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['testcase_id']; - $judgingRunInsertParams[':judgetaskid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['judgetaskid']; - } - $judgingRunInsertQuery = sprintf( - 'INSERT INTO judging_run (judgingid, testcaseid, judgetaskid) VALUES %s', - implode(', ', $judgingRunInsertParts) - ); - - $judgingRunInsertParamsWithoutColon = []; - foreach ($judgingRunInsertParams as $key => $param) { - $key = str_replace(':', '', $key); - $judgingRunInsertParamsWithoutColon[$key] = $param; - } - - $this->em->getConnection()->executeQuery($judgingRunInsertQuery, $judgingRunInsertParamsWithoutColon); + $this->actuallyCreateJudgetasks($priority, $judging); $team = $submission->getTeam(); $result = $this->em->createQueryBuilder() @@ -1399,7 +1305,7 @@ public function getAssetFiles(string $path): array * @param bool $fullPath If true, get the full path. If false, get the webserver relative path * @param string|null $forceExtension If set, also return the asset path if it does not exist currently and use the given extension */ - public function assetPath(string $name, string $type, bool $fullPath = false, ?string $forceExtension = null): ?string + public function assetPath(?string $name, string $type, bool $fullPath = false, ?string $forceExtension = null): ?string { $prefix = $fullPath ? ($this->getDomjudgeWebappDir() . '/public/') : ''; switch ($type) { @@ -1444,20 +1350,20 @@ public function globalBannerAssetPath(): ?string /** * Get the full asset path for the given entity and property. */ - public function fullAssetPath(AssetEntityInterface $entity, string $property, bool $useExternalid, ?string $forceExtension = null): ?string + public function fullAssetPath(AssetEntityInterface $entity, string $property, ?string $forceExtension = null): ?string { if ($entity instanceof Team && $property == 'photo') { - return $this->assetPath($useExternalid ? $entity->getExternalid() : (string)$entity->getTeamid(), 'team', true, $forceExtension); + return $this->assetPath($entity->getExternalid(), 'team', true, $forceExtension); } elseif ($entity instanceof TeamAffiliation && $property == 'logo') { - return $this->assetPath($useExternalid ? $entity->getExternalid() : (string)$entity->getAffilid(), 'affiliation', true, $forceExtension); + return $this->assetPath($entity->getExternalid(), 'affiliation', true, $forceExtension); } elseif ($entity instanceof Contest && $property == 'banner') { - return $this->assetPath($useExternalid ? $entity->getExternalid() : (string)$entity->getCid(), 'contest', true, $forceExtension); + return $this->assetPath($entity->getExternalid(), 'contest', true, $forceExtension); } return null; } - public function loadTeam(string $idField, string $teamId, Contest $contest): Team + public function loadTeam(string $teamId, Contest $contest): Team { $queryBuilder = $this->em->createQueryBuilder() ->from(Team::class, 't') @@ -1465,7 +1371,7 @@ public function loadTeam(string $idField, string $teamId, Contest $contest): Tea ->leftJoin('t.category', 'tc') ->leftJoin('t.contests', 'c') ->leftJoin('tc.contests', 'cc') - ->andWhere(sprintf('t.%s = :team', $idField)) + ->andWhere('t.externalid = :team') ->andWhere('t.enabled = 1') ->setParameter('team', $teamId); @@ -1642,4 +1548,122 @@ public function getScoreboardZip( return Utils::streamZipFile($tempFilename, 'contest.zip'); } + + private function allowJudge(ContestProblem $problem, Submission $submission, Language $language, bool $manualRequest): bool + { + if (!$problem->getAllowJudge() || !$language->getAllowJudge()) { + return false; + } + $evalOnDemand = false; + // We have 2 cases, the problem picks the global value or the value is set. + if ($problem->determineOnDemand($this->config->get('lazy_eval_results'))) { + $evalOnDemand = true; + + // Special case, we're shadow and someone submits on our side in that case + // we're not super lazy. + if ($this->shadowMode() && $submission->getExternalid() === ('dj-' . $submission->getSubmitid())) { + $evalOnDemand = false; + } + if ($manualRequest) { + // When explicitly requested, judge the submission. + $evalOnDemand = false; + } + } + return !$evalOnDemand; + } + + private function actuallyCreateJudgetasks(int $priority, Judging $judging): void + { + $submission = $judging->getSubmission(); + $problem = $submission->getContestProblem(); + // We use a mass insert query, since that is way faster than doing a separate insert for each testcase. + // We first insert judgetasks, then select their ID's and finally insert the judging runs. + + // Step 1: Create the template for the judgetasks. + $compileExecutable = $submission->getLanguage()->getCompileExecutable()->getImmutableExecutable(); + $judgetaskInsertParams = [ + ':type' => JudgeTaskType::JUDGING_RUN, + ':submitid' => $submission->getSubmitid(), + ':priority' => $priority, + ':jobid' => $judging->getJudgingid(), + ':uuid' => $judging->getUuid(), + ':compile_script_id' => $compileExecutable->getImmutableExecId(), + ':compare_script_id' => $this->getImmutableCompareExecutable($problem)->getImmutableExecId(), + ':run_script_id' => $this->getImmutableRunExecutable($problem)->getImmutableExecId(), + ':compile_config' => $this->getCompileConfig($submission), + ':run_config' => $this->getRunConfig($problem, $submission), + ':compare_config' => $this->getCompareConfig($problem), + ]; + + $judgetaskDefaultParamNames = array_keys($judgetaskInsertParams); + + // Step 2: Create and insert the judgetasks. + + $testcases = $problem->getProblem()->getTestcases(); + if (count($testcases) < 1) { + throw new BadRequestHttpException("No testcases set for problem {$problem->getProbid()}"); + } + $judgetaskInsertParts = []; + /** @var Testcase $testcase */ + foreach ($testcases as $testcase) { + $judgetaskInsertParts[] = sprintf( + '(%s, :testcase_id%d, :testcase_hash%d)', + implode(', ', $judgetaskDefaultParamNames), + $testcase->getTestcaseid(), + $testcase->getTestcaseid() + ); + $judgetaskInsertParams[':testcase_id' . $testcase->getTestcaseid()] = $testcase->getTestcaseid(); + $judgetaskInsertParams[':testcase_hash' . $testcase->getTestcaseid()] = $testcase->getMd5sumInput() . '_' . $testcase->getMd5sumOutput(); + } + $judgetaskColumns = array_map(fn(string $column) => substr($column, 1), $judgetaskDefaultParamNames); + $judgetaskInsertQuery = sprintf( + 'INSERT INTO judgetask (%s, testcase_id, testcase_hash) VALUES %s', + implode(', ', $judgetaskColumns), + implode(', ', $judgetaskInsertParts) + ); + + $judgetaskInsertParamsWithoutColon = []; + foreach ($judgetaskInsertParams as $key => $param) { + $key = str_replace(':', '', $key); + $judgetaskInsertParamsWithoutColon[$key] = $param; + } + + $this->em->getConnection()->executeQuery($judgetaskInsertQuery, $judgetaskInsertParamsWithoutColon); + + // Step 3: Fetch the judgetasks ID's per testcase. + $judgetaskData = $this->em->getConnection()->executeQuery( + 'SELECT judgetaskid, testcase_id FROM judgetask WHERE jobid = :jobid ORDER BY judgetaskid', + ['jobid' => $judging->getJudgingid()] + )->fetchAllAssociative(); + + // Step 4: Create and insert the corresponding judging runs. + $judgingRunInsertParams = [':judgingid' => $judging->getJudgingid()]; + $judgingRunInsertParts = []; + foreach ($judgetaskData as $judgetaskItem) { + $judgingRunInsertParts[] = sprintf( + '(:judgingid, :testcaseid%d, :judgetaskid%d)', + $judgetaskItem['judgetaskid'], + $judgetaskItem['judgetaskid'] + ); + $judgingRunInsertParams[':testcaseid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['testcase_id']; + $judgingRunInsertParams[':judgetaskid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['judgetaskid']; + } + $judgingRunInsertQuery = sprintf( + 'INSERT INTO judging_run (judgingid, testcaseid, judgetaskid) VALUES %s', + implode(', ', $judgingRunInsertParts) + ); + + $judgingRunInsertParamsWithoutColon = []; + foreach ($judgingRunInsertParams as $key => $param) { + $key = str_replace(':', '', $key); + $judgingRunInsertParamsWithoutColon[$key] = $param; + } + + $this->em->getConnection()->executeQuery($judgingRunInsertQuery, $judgingRunInsertParamsWithoutColon); + } + + public function shadowMode(): bool + { + return (bool)$this->config->get('shadow_mode'); + } } diff --git a/webapp/src/Service/EventLogService.php b/webapp/src/Service/EventLogService.php index 9134c33d109..b2f053858e2 100644 --- a/webapp/src/Service/EventLogService.php +++ b/webapp/src/Service/EventLogService.php @@ -7,6 +7,7 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Event; +use App\Entity\HasExternalIdInterface; use App\Entity\Judging; use App\Entity\JudgingRun; use App\Entity\Submission; @@ -30,110 +31,66 @@ class EventLogService { // Keys used in below config: - final public const KEY_TYPE = 'type'; - final public const KEY_URL = 'url'; final public const KEY_ENTITY = 'entity'; final public const KEY_TABLES = 'tables'; - final public const KEY_USE_EXTERNAL_ID = 'use-external-id'; - final public const KEY_ALWAYS_USE_EXTERNAL_ID = 'always-use-external-id'; - final public const KEY_SKIP_IN_EVENT_FEED = 'skip-in-event-feed'; - // Types of endpoints: - final public const TYPE_CONFIGURATION = 'configuration'; - final public const TYPE_LIVE = 'live'; - final public const TYPE_AGGREGATE = 'aggregate'; // Allowed actions: final public const ACTION_CREATE = 'create'; final public const ACTION_UPDATE = 'update'; final public const ACTION_DELETE = 'delete'; - // TODO: Add a way to specify when to use external ID using some (DB) - // config instead of hardcoding it here. Also relates to - // AbstractRestController::getIdField. /** @var mixed[] */ public array $apiEndpoints = [ - 'contests' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, - self::KEY_URL => '', - self::KEY_USE_EXTERNAL_ID => true, - ], + 'contests' => [], 'judgement-types' => [ // hardcoded in $VERDICTS and the API - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], - 'languages' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, - self::KEY_USE_EXTERNAL_ID => true, - self::KEY_ALWAYS_USE_EXTERNAL_ID => true, - ], + 'languages' => [], 'problems' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_TABLES => ['problem', 'contestproblem'], - self::KEY_USE_EXTERNAL_ID => true, ], 'groups' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => TeamCategory::class, self::KEY_TABLES => ['team_category'], - self::KEY_USE_EXTERNAL_ID => true, ], 'organizations' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => TeamAffiliation::class, self::KEY_TABLES => ['team_affiliation'], - self::KEY_USE_EXTERNAL_ID => true, ], 'teams' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_TABLES => ['team', 'contestteam'], - self::KEY_USE_EXTERNAL_ID => true, ], 'state' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], - 'submissions' => [ - self::KEY_TYPE => self::TYPE_LIVE, - self::KEY_USE_EXTERNAL_ID => true, - ], + 'submissions' => [], 'judgements' => [ - self::KEY_TYPE => self::TYPE_LIVE, self::KEY_ENTITY => Judging::class, self::KEY_TABLES => ['judging'], ], 'runs' => [ - self::KEY_TYPE => self::TYPE_LIVE, self::KEY_ENTITY => JudgingRun::class, self::KEY_TABLES => ['judging_run'], ], - 'clarifications' => [ - self::KEY_TYPE => self::TYPE_LIVE, - self::KEY_USE_EXTERNAL_ID => true, - ], + 'clarifications' => [], 'awards' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], 'scoreboard' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], 'event-feed' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => ['event'], ], 'accounts' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => User::class, self::KEY_TABLES => ['user'], - self::KEY_USE_EXTERNAL_ID => true, - self::KEY_SKIP_IN_EVENT_FEED => true, ], ]; @@ -153,9 +110,6 @@ public function __construct( protected readonly LoggerInterface $logger ) { foreach ($this->apiEndpoints as $endpoint => $data) { - if (!array_key_exists(self::KEY_URL, $data)) { - $this->apiEndpoints[$endpoint][self::KEY_URL] = '/' . $endpoint; - } if (!array_key_exists(self::KEY_ENTITY, $data)) { // Determine default controller $inflector = InflectorFactory::create()->build(); @@ -265,12 +219,6 @@ public function log( ); return; } - if ($endpoint[self::KEY_URL] === null) { - $this->logger->warning( - "EventLogService::log: no endpoint for '%s', ignoring", [ $type ] - ); - return; - } // Look up external/API ID from various sources. if ($ids === null) { @@ -355,12 +303,12 @@ public function log( if ($action === self::ACTION_DELETE) { $json = array_values(array_map(fn($id) => ['id' => (string)$id], $ids)); } elseif ($json === null) { - $url = $endpoint[self::KEY_URL]; + $url = $type === 'contests' ? '' : ('/' . $type); // Temporary fix for single/multi contest API: if (isset($contestId)) { - $externalContestIds = $this->getExternalIds('contests', [$contestId]); - $url = '/contests/' . reset($externalContestIds) . $url; + $externalContestId = $this->em->getRepository(Contest::class)->find($contestId)->getExternalid(); + $url = '/contests/' . $externalContestId . $url; } if (in_array($type, ['contests', 'state'])) { @@ -470,12 +418,14 @@ public function addMissingStateEvents(Contest $contest): void // Because some states can happen in multiple different orders, we need to check per // field to see if we have a state event where that field matches the current value. + // If the contest start is disabled, all fields should be null. + // Note that for `started` the check already happens within the method, but we can better be explicit here. $states = [ - 'started' => $contest->getStarttime(), - 'ended' => $contest->getEndtime(), - 'frozen' => $contest->getFreezetime(), - 'thawed' => $contest->getUnfreezetime(), - 'finalized' => $contest->getFinalizetime(), + 'started' => $contest->getStarttimeEnabled() ? $contest->getStarttime() : null, + 'ended' => $contest->getStarttimeEnabled() ? $contest->getEndtime() : null, + 'frozen' => $contest->getStarttimeEnabled() ? $contest->getFreezetime() : null, + 'thawed' => $contest->getStarttimeEnabled() ? $contest->getUnfreezetime() : null, + 'finalized' => $contest->getStarttimeEnabled() ? $contest->getFinalizetime() : null, ]; // Because we have the events in order now, we can keep 'growing' the data to insert, @@ -533,7 +483,7 @@ public function addMissingStateEvents(Contest $contest): void if ($field === 'finalized') { // Insert all awards events. - $url = sprintf('/contests/%s/awards', $contest->getApiId($this)); + $url = sprintf('/contests/%s/awards', $contest->getExternalid()); $awards = []; $this->dj->withAllRoles(function () use ($url, &$awards) { $awards = $this->dj->internalApiRequest($url); @@ -658,19 +608,24 @@ protected function insertEvent( */ public function initStaticEvents(Contest $contest): void { + $staticEventTypes = [ + 'contests', + 'judgement-types', + 'languages', + 'problems', + 'groups', + 'organizations', + 'teams', + ]; // Loop over all configuration endpoints with an URL and check if we have all data. foreach ($this->apiEndpoints as $endpoint => $endpointData) { - if ($endpointData[static::KEY_SKIP_IN_EVENT_FEED] ?? false) { - continue; - } - if ($endpointData[EventLogService::KEY_TYPE] === EventLogService::TYPE_CONFIGURATION && - isset($endpointData[EventLogService::KEY_URL])) { - $contestId = $contest->getApiId($this); + if (in_array($endpoint, $staticEventTypes, true)) { + $contestId = $contest->getExternalid(); // Do an internal API request to the overview URL // of the endpoint to get current data. - $url = sprintf('/contests/%s%s', $contestId, - $endpointData[EventLogService::KEY_URL]); + $urlPart = $endpoint === 'contests' ? '' : ('/' . $endpoint); + $url = sprintf('/contests/%s%s', $contestId, $urlPart); $this->dj->withAllRoles(function () use ($url, &$data) { $data = $this->dj->internalApiRequest($url); }); @@ -737,7 +692,7 @@ protected function hasAllDependentObjectEvents(Contest $contest, string $type, a { // Build up the referenced data to check for. $toCheck = [ - 'contests' => $contest->getApiId($this), + 'contests' => $contest->getExternalid(), ]; switch ($type) { case 'teams': @@ -844,9 +799,6 @@ protected function getExternalIds(string $type, array $ids): array } $endpointData = $this->apiEndpoints[$type]; - if (!isset($endpointData[self::KEY_USE_EXTERNAL_ID])) { - return $ids; - } /** @var class-string $entity */ $entity = $endpointData[self::KEY_ENTITY]; @@ -854,25 +806,7 @@ protected function getExternalIds(string $type, array $ids): array throw new BadMethodCallException(sprintf('No entity defined for type \'%s\'', $type)); } - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID. - $externalIdAlwaysAllowed = [ - Submission::class => 's.submitid', - Clarification::class => 'clar.clarid', - ]; - if (isset($externalIdAlwaysAllowed[$entity])) { - $fullField = $externalIdAlwaysAllowed[$entity]; - [$table, $field] = explode('.', $fullField); - return array_map(fn(array $item) => $item['externalid'] ?? $item[$field], $this->em->createQueryBuilder() - ->from($entity, $table) - ->select($fullField, sprintf('%s.externalid', $table)) - ->andWhere(sprintf('%s IN (:ids)', $fullField)) - ->setParameter('ids', $ids) - ->getQuery() - ->getScalarResult()); - } - - if (!$this->externalIdFieldForEntity($entity)) { + if (!is_a($entity, HasExternalIdInterface::class, true)) { return $ids; } @@ -896,80 +830,6 @@ protected function getExternalIds(string $type, array $ids): array ); } - /** - * Get the external ID field for a given entity type. Will return null if - * no external ID field should be used. - * @param object|string $entity - */ - public function externalIdFieldForEntity($entity): ?string - { - // Allow passing in a class instance: convert it to its class type. - if (is_object($entity)) { - $entity = $entity::class; - } - // Special case: strip of Doctrine proxies. - if (str_starts_with($entity, 'Proxies\\__CG__\\')) { - $entity = substr($entity, strlen('Proxies\\__CG__\\')); - } - - if (!isset($this->entityToEndpoint[$entity])) { - throw new BadMethodCallException(sprintf('Entity \'%s\' does not have a corresponding endpoint', - $entity)); - } - - $endpointData = $this->apiEndpoints[$this->entityToEndpoint[$entity]]; - - if (!isset($endpointData[self::KEY_USE_EXTERNAL_ID])) { - return null; - } - - $useExternalId = false; - if ($endpointData[self::KEY_ALWAYS_USE_EXTERNAL_ID] ?? false) { - $useExternalId = true; - } else { - $dataSource = $this->config->get('data_source'); - - if ($dataSource !== DOMJudgeService::DATA_SOURCE_LOCAL) { - $endpointType = $endpointData[self::KEY_TYPE]; - if ($endpointType === self::TYPE_CONFIGURATION && - in_array($dataSource, [ - DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL, - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL - ])) { - $useExternalId = true; - } elseif ($endpointType === self::TYPE_LIVE && - $dataSource === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL) { - $useExternalId = true; - } - } - } - - if ($useExternalId) { - return 'externalid'; - } else { - return null; - } - } - - /** - * Get the API ID field for a given entity type. - * @param object|string $entity - */ - public function apiIdFieldForEntity($entity): string - { - if ($field = $this->externalIdFieldForEntity($entity)) { - return $field; - } - /** @var class-string $class */ - $class = is_object($entity) ? $entity::class : $entity; - $metadata = $this->em->getClassMetadata($class); - try { - return $metadata->getSingleIdentifierFieldName(); - } catch (MappingException) { - throw new BadMethodCallException("Entity '$class' has a composite primary key"); - } - } - /** * Get the endpoint to use for the given entity. * @param object|string $entity diff --git a/webapp/src/Service/ExternalContestSourceService.php b/webapp/src/Service/ExternalContestSourceService.php index aab49ee951b..54e2d96d7c9 100644 --- a/webapp/src/Service/ExternalContestSourceService.php +++ b/webapp/src/Service/ExternalContestSourceService.php @@ -407,10 +407,13 @@ protected function importFromCcsApi(array $eventsToSkip, ?callable $progressRepo $this->setLastEvent($this->getLastReadEventId()); } } catch (TransportException $e) { - $this->logger->error( - 'Received error while reading event feed: %s', - [$e->getMessage()] - ); + if (!str_starts_with($e->getMessage(), 'OpenSSL SSL_read: error:0A000126')) { + // Ignore error of not fully compliant TLS implementation on server-side + $this->logger->error( + 'Received error while reading event feed: %s', + [$e->getMessage()] + ); + } } } @@ -620,7 +623,7 @@ public function importEvent(Event $event, array $eventsToSkip): void // Note the @vars here are to make PHPStan understand the correct types. $method = match ($event->type) { - EventType::AWARDS, EventType::TEAM_MEMBERS, EventType::ACCOUNTS, EventType::PERSONS => $this->ignoreEvent(...), + EventType::ACCOUNTS, EventType::AWARDS, EventType::MAP_INFO, EventType::PERSONS, EventType::TEAM_MEMBERS => $this->ignoreEvent(...), EventType::STATE => $this->validateState(...), EventType::CONTESTS => $this->validateAndUpdateContest(...), EventType::JUDGEMENT_TYPES => $this->importJudgementType(...), @@ -748,8 +751,8 @@ protected function validateAndUpdateContest(Event $event, EventData $data): void $toCheck = [ 'start_time_enabled' => true, 'start_time_string' => $startTime->format('Y-m-d H:i:s ') . $timezoneToUse, - 'end_time' => $contest->getAbsoluteTime($fullDuration), - 'freeze_time' => $contest->getAbsoluteTime($fullFreeze), + 'end_time_string' => preg_replace('/\.000$/', '', $fullDuration), + 'freeze_time_string' => preg_replace('/\.000$/', '', $fullFreeze), ]; } else { $toCheck = [ diff --git a/webapp/src/Service/ImportExportService.php b/webapp/src/Service/ImportExportService.php index b99c6189bf1..6d60164766b 100644 --- a/webapp/src/Service/ImportExportService.php +++ b/webapp/src/Service/ImportExportService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\DataTransferObject\ResultRow; use App\Entity\Configuration; use App\Entity\Contest; use App\Entity\ContestProblem; @@ -63,6 +64,14 @@ public function getContestYamlData(Contest $contest, bool $includeProblems = tru if ($warnMsg = $contest->getWarningMessage()) { $data['warning_message'] = $warnMsg; } + + foreach (['gold', 'silver', 'bronze'] as $medal) { + $medalCount = $contest->{'get' . ucfirst($medal) . 'Medals'}(); + if ($medalCount) { + $data['medals'][$medal] = $medalCount; + } + } + if ($contest->getFreezetime() !== null) { $data['scoreboard_freeze_time'] = Utils::absTime($contest->getFreezetime(), true); $data['scoreboard_freeze_duration'] = Utils::relTime( @@ -209,10 +218,11 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, st $data['short_name'] ?? $data['shortname'] ?? $data['short-name'] ?? $data['id'] )) ->setExternalid($contest->getShortname()) - ->setWarningMessage($data['warning-message'] ?? null) + ->setWarningMessage($data['warning_message'] ?? $data['warning-message'] ?? null) ->setStarttimeString(date_format($startTime, 'Y-m-d H:i:s e')) ->setActivatetimeString(date_format($activateTime, 'Y-m-d H:i:s e')) - ->setEndtimeString(sprintf('+%s', $data['duration'])); + ->setEndtimeString(sprintf('+%s', $data['duration'])) + ->setPublic($data['public'] ?? true); if ($deactivateTime) { $contest->setDeactivatetimeString(date_format($deactivateTime, 'Y-m-d H:i:s e')); } @@ -228,6 +238,13 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, st ->setMedalsEnabled(true) ->addMedalCategory($visibleCategory); } + + foreach (['gold', 'silver', 'bronze'] as $medal) { + if (isset($data['medals'][$medal])) { + $setter = 'set' . ucfirst($medal) . 'Medals'; + $contest->$setter($data['medals'][$medal]); + } + } } /** @var string|null $freezeDuration */ @@ -296,7 +313,7 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, st $this->importProblemsData($contest, $data['problems']); } - $cid = $contest->getApiId($this->eventLogService); + $cid = $contest->getExternalid(); $this->em->flush(); return true; @@ -308,8 +325,9 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, st * problems?: array{name?: string, short-name?: string, id?: string, label?: string, * letter?: string, label?: string, letter?: string}} $problems * @param string[]|null $ids + * @param array $messages */ - public function importProblemsData(Contest $contest, array $problems, array &$ids = null): bool + public function importProblemsData(Contest $contest, array $problems, array &$ids = null, ?array &$messages = []): bool { // For problemset.yaml the root key is called `problems`, so handle that case // TODO: Move this check away to make the $problems array shape easier @@ -328,8 +346,19 @@ public function importProblemsData(Contest $contest, array $problems, array &$id ->setTimelimit($problemData['time_limit'] ?? 10) ->setExternalid($problemData['id'] ?? $problemData['short-name'] ?? $problemLabel ?? null); - $this->em->persist($problem); - $this->em->flush(); + $errors = $this->validator->validate($problem); + $hasProblemErrors = $errors->count(); + if ($hasProblemErrors) { + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages['danger'][] = sprintf( + 'Error: problems.%s.%s: %s', + $problem->getExternalid(), + $error->getPropertyPath(), + $error->getMessage() + ); + } + } $contestProblem = new ContestProblem(); $contestProblem @@ -338,14 +367,34 @@ public function importProblemsData(Contest $contest, array $problems, array &$id // We need to set both the entities and the IDs because of the composite primary key. ->setProblem($problem) ->setContest($contest); + + $errors = $this->validator->validate($contestProblem); + $hasContestProblemErrors = $errors->count(); + if ($hasContestProblemErrors) { + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages['danger'][] = sprintf( + 'Error: problems.%s.contestproblem.%s: %s', + $problem->getExternalid(), + $error->getPropertyPath(), + $error->getMessage() + ); + } + } + + if ($hasProblemErrors || $hasContestProblemErrors) { + return false; + } + + $this->em->persist($problem); $this->em->persist($contestProblem); + $this->em->flush(); - $ids[] = $problem->getApiId($this->eventLogService); + $ids[] = $problem->getExternalid(); } $this->em->flush(); - // For now this method will never fail so always return true. return true; } @@ -366,7 +415,7 @@ public function getGroupData(): array $data = []; foreach ($categories as $category) { - $data[] = [$category->getApiId($this->eventLogService), $category->getName()]; + $data[] = [$category->getExternalid(), $category->getName()]; } return $data; @@ -391,9 +440,9 @@ public function getTeamData(): array $data = []; foreach ($teams as $team) { $data[] = [ - $team->getApiId($this->eventLogService), + $team->getExternalid(), $team->getIcpcId(), - $team->getCategory()->getApiId($this->eventLogService), + $team->getCategory()->getExternalid(), $team->getEffectiveName(), $team->getAffiliation() ? $team->getAffiliation()->getName() : '', $team->getAffiliation() ? $team->getAffiliation()->getShortname() : '', @@ -406,21 +455,13 @@ public function getTeamData(): array } /** - * Get results data for the given sortorder. - * - * We'll here assume that the requested file will be of the current contest, - * as all our scoreboard interfaces do: - * 0 ICPC ID 24314 string - * 1 Rank in contest 1 integer|'' - * 2 Award Gold Medal string - * 3 Number of problems the team has solved 4 integer - * 4 Total Time 534 integer - * 5 Time of the last submission 233 integer - * 6 Group Winner North American string - * @return array + * @return ResultRow[] */ - public function getResultsData(int $sortOrder, bool $full = false): array - { + public function getResultsData( + int $sortOrder, + bool $individuallyRanked = false, + bool $honors = true, + ): array { $contest = $this->dj->getCurrentContest(); if ($contest === null) { throw new BadRequestHttpException('No current contest'); @@ -464,9 +505,14 @@ public function getResultsData(int $sortOrder, bool $full = false): array } } - $ranks = []; - $groupWinners = []; - $data = []; + $ranks = []; + $groupWinners = []; + $data = []; + $lowestMedalPoints = 0; + + // 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 ($teamScore->team->getCategory()->getSortorder() !== $sortOrder) { @@ -480,72 +526,102 @@ public function getResultsData(int $sortOrder, bool $full = false): array $rank = $teamScore->rank; $numPoints = $teamScore->numPoints; - if ($rank <= $contest->getGoldMedals()) { + $skip = false; + + if (!$contest->getMedalCategories()->contains($teamScore->team->getCategory())) { + $skip = true; + $skippedTeams++; + } + + if ($numPoints === 0) { + // Teams with 0 points won't get a medal, a rank or an honor. + // They will always get an honorable mention. + $data[] = new ResultRow( + $teamScore->team->getIcpcId(), + null, + 'Honorable', + $teamScore->numPoints, + $teamScore->totalTime, + $maxTime, + null + ); + continue; + } + + if (!$skip && $rank - $skippedTeams <= $contest->getGoldMedals()) { $awardString = 'Gold Medal'; - } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals()) { + $lowestMedalPoints = $teamScore->numPoints; + } elseif (!$skip && $rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals()) { $awardString = 'Silver Medal'; - } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $contest->getB()) { + $lowestMedalPoints = $teamScore->numPoints; + } elseif (!$skip && $rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $contest->getB()) { $awardString = 'Bronze Medal'; + $lowestMedalPoints = $teamScore->numPoints; } elseif ($numPoints >= $median) { // Teams with equally solved number of problems get the same rank unless $full is true. - if (!$full) { + if (!$individuallyRanked) { if (!isset($ranks[$numPoints])) { $ranks[$numPoints] = $rank; } $rank = $ranks[$numPoints]; } - $awardString = 'Ranked'; + if ($honors) { + if ($numPoints === $lowestMedalPoints + || $rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $contest->getB()) { + // Some teams out of the medal categories but ranked higher than bronze medallists may get more points. + $awardString = 'Highest Honors'; + } elseif ($numPoints === $lowestMedalPoints - 1) { + $awardString = 'High Honors'; + } else { + $awardString = 'Honors'; + } + } else { + $awardString = 'Ranked'; + } } else { $awardString = 'Honorable'; - $rank = ''; + $rank = null; } - $groupWinner = ""; $categoryId = $teamScore->team->getCategory()->getCategoryid(); - if (!isset($groupWinners[$categoryId])) { + if (isset($groupWinners[$categoryId])) { + $groupWinner = null; + } else { $groupWinners[$categoryId] = true; $groupWinner = $teamScore->team->getCategory()->getName(); } - $data[] = [ + $data[] = new ResultRow( $teamScore->team->getIcpcId(), $rank, $awardString, $teamScore->numPoints, $teamScore->totalTime, $maxTime, - $groupWinner - ]; + $groupWinner, + ); } // Sort by rank/name. - uasort($data, function ($a, $b) use ($teams) { - if ($a[1] != $b[1]) { + uasort($data, function (ResultRow $a, ResultRow $b) use ($teams) { + if ($a->rank !== $b->rank) { // Honorable mention has no rank. - if ($a[1] === '') { + if ($a->rank === null) { return 1; - } elseif ($b[1] === '') { + } elseif ($b->rank === null) { return -11; } - return $a[1] - $b[1]; - } - $teamA = $teams[$a[0]] ?? null; - $teamB = $teams[$b[0]] ?? null; - if ($teamA) { - $nameA = $teamA->getEffectiveName(); - } else { - $nameA = ''; - } - if ($teamB) { - $nameB = $teamB->getEffectiveName(); - } else { - $nameB = ''; + return $a->rank <=> $b->rank; } + $teamA = $teams[$a->teamId] ?? null; + $teamB = $teams[$b->teamId] ?? null; + $nameA = $teamA?->getEffectiveName(); + $nameB = $teamB?->getEffectiveName(); $collator = new Collator('en'); return $collator->compare($nameA, $nameB); }); - return $data; + return array_values($data); } /** @@ -643,7 +719,7 @@ protected function importGroupsTsv(array $content, ?string &$message = null): in /** * Import groups JSON * - * @param array $data * @param TeamCategory[]|null $saved The saved groups */ @@ -655,7 +731,7 @@ public function importGroupsJson(array $data, ?string &$message = null, ?array & $groupData[] = [ 'categoryid' => @$group['id'], 'icpc_id' => @$group['icpc_id'], - 'name' => @$group['name'], + 'name' => $group['name'] ?? '', 'visible' => !($group['hidden'] ?? false), 'sortorder' => @$group['sortorder'], 'color' => @$group['color'], @@ -663,7 +739,7 @@ public function importGroupsJson(array $data, ?string &$message = null, ?array & ]; } - return $this->importGroupData($groupData, $saved); + return $this->importGroupData($groupData, $saved, $message); } /** @@ -675,20 +751,24 @@ public function importGroupsJson(array $data, ?string &$message = null, ?array & * * @throws NonUniqueResultException */ - protected function importGroupData(array $groupData, ?array &$saved = null): int - { + protected function importGroupData( + array $groupData, + ?array &$saved = null, + ?string &$message = null + ): int { // We want to overwrite the ID so change the ID generator. $createdCategories = []; $updatedCategories = []; + $allCategories = []; + $anyErrors = []; - foreach ($groupData as $groupItem) { + foreach ($groupData as $index => $groupItem) { if (empty($groupItem['categoryid'])) { $categoryId = null; $teamCategory = null; } else { $categoryId = $groupItem['categoryid']; - $field = $this->eventLogService->apiIdFieldForEntity(TeamCategory::class); - $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy([$field => $categoryId]); + $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $categoryId]); } $added = false; if (!$teamCategory) { @@ -696,7 +776,6 @@ protected function importGroupData(array $groupData, ?array &$saved = null): int if ($categoryId !== null) { $teamCategory->setExternalid($categoryId); } - $this->em->persist($teamCategory); $added = true; } $teamCategory @@ -706,19 +785,41 @@ protected function importGroupData(array $groupData, ?array &$saved = null): int ->setColor($groupItem['color'] ?? null) ->setIcpcid($groupItem['icpc_id'] ?? null); $teamCategory->setAllowSelfRegistration($groupItem['allow_self_registration']); - $this->em->flush(); - $this->dj->auditlog('team_category', $teamCategory->getCategoryid(), 'replaced', - 'imported from tsv / json'); - if ($added) { - $createdCategories[] = $teamCategory->getCategoryid(); + + $errors = $this->validator->validate($teamCategory); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Group at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; } else { - $updatedCategories[] = $teamCategory->getCategoryid(); - } - if ($saved !== null) { - $saved[] = $teamCategory; + $allCategories[] = $teamCategory; + if ($added) { + $createdCategories[] = $teamCategory->getCategoryid(); + } else { + $updatedCategories[] = $teamCategory->getCategoryid(); + } + if ($saved !== null) { + $saved[] = $teamCategory; + } } } + if ($anyErrors) { + return 0; + } + + foreach ($allCategories as $category) { + $this->em->persist($category); + $this->em->flush(); + $this->dj->auditlog('team_category', $category->getCategoryid(), 'replaced', + 'imported from tsv / json'); + } + if ($contest = $this->dj->getCurrentContest()) { if (!empty($createdCategories)) { $this->eventLogService->log('team_category', $createdCategories, 'create', $contest->getCid(), null, null, false); @@ -752,7 +853,7 @@ public function importOrganizationsJson(array $data, ?string &$message = null, ? ]; } - return $this->importOrganizationData($organizationData, $saved); + return $this->importOrganizationData($organizationData, $saved, $message); } /** @@ -763,11 +864,16 @@ public function importOrganizationsJson(array $data, ?string &$message = null, ? * * @throws NonUniqueResultException */ - protected function importOrganizationData(array $organizationData, ?array &$saved = null): int - { + protected function importOrganizationData( + array $organizationData, + ?array &$saved = null, + ?string &$message = null, + ): int { $createdOrganizations = []; $updatedOrganizations = []; - foreach ($organizationData as $organizationItem) { + $allOrganizations = []; + $anyErrors = false; + foreach ($organizationData as $index => $organizationItem) { $externalId = $organizationItem['externalid']; $teamAffiliation = null; $added = false; @@ -777,7 +883,6 @@ protected function importOrganizationData(array $organizationData, ?array &$save if (!$teamAffiliation) { $teamAffiliation = new TeamAffiliation(); $teamAffiliation->setExternalid($externalId); - $this->em->persist($teamAffiliation); $added = true; } if (!isset($organizationItem['shortname'])) { @@ -788,19 +893,40 @@ protected function importOrganizationData(array $organizationData, ?array &$save ->setName($organizationItem['name']) ->setCountry($organizationItem['country']) ->setIcpcid($organizationItem['icpc_id'] ?? null); - $this->em->flush(); - if ($added) { - $createdOrganizations[] = $teamAffiliation->getAffilid(); + $errors = $this->validator->validate($teamAffiliation); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Organization at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; } else { - $updatedOrganizations[] = $teamAffiliation->getAffilid(); - } - $this->dj->auditlog('team_affiliation', $teamAffiliation->getAffilid(), 'replaced', - 'imported from tsv / json'); - if ($saved !== null) { - $saved[] = $teamAffiliation; + $allOrganizations[] = $teamAffiliation; + if ($added) { + $createdOrganizations[] = $teamAffiliation->getAffilid(); + } else { + $updatedOrganizations[] = $teamAffiliation->getAffilid(); + } + if ($saved !== null) { + $saved[] = $teamAffiliation; + } } } + if ($anyErrors) { + return 0; + } + + foreach ($allOrganizations as $organization) { + $this->em->persist($organization); + $this->em->flush(); + $this->dj->auditlog('team_affiliation', $organization->getAffilid(), 'replaced', + 'imported from tsv / json'); + } + if ($contest = $this->dj->getCurrentContest()) { if (!empty($createdOrganizations)) { $this->eventLogService->log('team_affiliation', $createdOrganizations, 'create', $contest->getCid(), null, null, false); @@ -936,13 +1062,6 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array $type = $account['type']; $username = $account['username']; - $icpcRegexChars = "[a-zA-Z0-9@._-]"; - $icpcRegex = "/^" . $icpcRegexChars . "+$/"; - if (!preg_match($icpcRegex, $username)) { - $message = sprintf('Username "%s" should be non empty and only contain: %s', $username, $icpcRegexChars); - return -1; - } - // Special case for the World Finals, if the username is CDS we limit the access. // The user can see what every admin can see, but can not log in via the UI. if (isset($account['username']) && $account['username'] === 'cds') { @@ -985,7 +1104,7 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array ]; } - return $this->importAccountData($accountData, $saved); + return $this->importAccountData($accountData, $saved, $message); } /** @@ -1003,9 +1122,12 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array protected function importTeamData(array $teamData, ?string &$message, ?array &$saved = null): int { $createdAffiliations = []; + $createdCategories = []; $createdTeams = []; $updatedTeams = []; - foreach ($teamData as $teamItem) { + $allTeams = []; + $anyErrors = false; + foreach ($teamData as $index => $teamItem) { // It is legitimate that a team has no affiliation. Do not add it then. $teamAffiliation = null; $teamCategory = null; @@ -1019,11 +1141,19 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $propertyAccessor->setValue($teamAffiliation, $field, $value); } - $this->em->persist($teamAffiliation); - $this->em->flush(); - $createdAffiliations[] = $teamAffiliation->getAffilid(); - $this->dj->auditlog('team_affiliation', $teamAffiliation->getAffilid(), - 'added', 'imported from tsv'); + $errors = $this->validator->validate($teamAffiliation); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Organization for team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $createdAffiliations[] = $teamAffiliation; + } } } elseif (!empty($teamItem['team_affiliation']['externalid'])) { $teamAffiliation = $this->em->getRepository(TeamAffiliation::class)->findOneBy(['externalid' => $teamItem['team_affiliation']['externalid']]); @@ -1033,26 +1163,46 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s ->setExternalid($teamItem['team_affiliation']['externalid']) ->setName($teamItem['team_affiliation']['externalid'] . ' - auto-create during import') ->setShortname($teamItem['team_affiliation']['externalid'] . ' - auto-create during import'); - $this->em->persist($teamAffiliation); - $this->dj->auditlog('team_affiliation', - $teamAffiliation->getAffilid(), - 'added', 'imported from tsv / json'); + + $errors = $this->validator->validate($teamAffiliation); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Organization for team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $createdAffiliations[] = $teamAffiliation; + } } } $teamItem['team']['affiliation'] = $teamAffiliation; unset($teamItem['team']['affilid']); if (!empty($teamItem['team']['categoryid'])) { - $field = $this->eventLogService->apiIdFieldForEntity(TeamCategory::class); - $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy([$field => $teamItem['team']['categoryid']]); + $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $teamItem['team']['categoryid']]); if (!$teamCategory) { $teamCategory = new TeamCategory(); $teamCategory ->setExternalid($teamItem['team']['categoryid']) ->setName($teamItem['team']['categoryid'] . ' - auto-create during import'); - $this->em->persist($teamCategory); - $this->dj->auditlog('team_category', $teamCategory->getCategoryid(), - 'added', 'imported from tsv'); + + $errors = $this->validator->validate($teamCategory); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Group for team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $createdCategories[] = $teamCategory; + } } } $teamItem['team']['category'] = $teamCategory; @@ -1062,8 +1212,7 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s if (empty($teamItem['team']['teamid'])) { $team = null; } else { - $field = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $team = $this->em->getRepository(Team::class)->findOneBy([$field => $teamItem['team']['teamid']]); + $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamItem['team']['teamid']]); } if (!$team) { $team = new Team(); @@ -1082,11 +1231,6 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s return -1; } - if (!$teamItem['team']['name']) { - $message = 'Name for team required'; - return -1; - } - $team->setExternalid($teamItem['team']['teamid']); unset($teamItem['team']['teamid']); @@ -1095,10 +1239,19 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $propertyAccessor->setValue($team, $field, $value); } - if ($added) { - $this->em->persist($team); + $errors = $this->validator->validate($team); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $allTeams[] = $team; } - $this->em->flush(); if ($added) { $createdTeams[] = $team->getTeamid(); @@ -1106,15 +1259,40 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $updatedTeams[] = $team->getTeamid(); } - $this->dj->auditlog('team', $team->getTeamid(), 'replaced', 'imported from tsv'); if ($saved !== null) { $saved[] = $team; } } + if ($anyErrors) { + return 0; + } + + foreach ($createdAffiliations as $affiliation) { + $this->em->persist($affiliation); + $this->em->flush(); + $this->dj->auditlog('team_affiliation', + $affiliation->getAffilid(), + 'added', 'imported from tsv / json'); + } + + foreach ($createdCategories as $category) { + $this->em->persist($category); + $this->em->flush(); + $this->dj->auditlog('team_category', $category->getCategoryid(), + 'added', 'imported from tsv'); + } + + foreach ($allTeams as $team) { + $this->em->persist($team); + $this->em->flush(); + $this->dj->auditlog('team', $team->getTeamid(), 'replaced', 'imported from tsv'); + } + if ($contest = $this->dj->getCurrentContest()) { if (!empty($createdAffiliations)) { - $this->eventLogService->log('team_affiliation', $createdAffiliations, + $affiliationIds = array_map(fn (TeamAffiliation $affiliation) => $affiliation->getAffilid(), $createdAffiliations); + $this->eventLogService->log('team_affiliation', $affiliationIds, 'create', $contest->getCid()); } if (!empty($createdTeams)) { @@ -1140,10 +1318,15 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s * publicdescription?: string}}> $accountData * @throws NonUniqueResultException */ - protected function importAccountData(array $accountData, ?array &$saved = null): int - { + protected function importAccountData( + array $accountData, + ?array &$saved = null, + ?string &$message = null + ): int { $newTeams = []; - foreach ($accountData as $accountItem) { + $anyErrors = false; + $allUsers = []; + foreach ($accountData as $index => $accountItem) { if (!empty($accountItem['team'])) { $team = $this->em->getRepository(Team::class)->findOneBy([ 'name' => $accountItem['team']['name'], @@ -1156,28 +1339,33 @@ protected function importAccountData(array $accountData, ?array &$saved = null): ->setCategory($accountItem['team']['category']) ->setExternalid($accountItem['team']['externalid']) ->setPublicDescription($accountItem['team']['publicdescription'] ?? null); - $this->em->persist($team); $action = EventLogService::ACTION_CREATE; } else { $action = EventLogService::ACTION_UPDATE; } - $this->em->flush(); - $newTeams[] = [ - 'team' => $team, - 'action' => $action, - ]; - $this->dj->auditlog('team', $team->getTeamid(), 'replaced', - 'imported from tsv, autocreated for judge'); + $errors = $this->validator->validate($team); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Team for user at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $newTeams[] = [ + 'team' => $team, + 'action' => $action, + ]; + } $accountItem['user']['team'] = $team; unset($accountItem['user']['teamid']); } $user = $this->em->getRepository(User::class)->findOneBy(['username' => $accountItem['user']['username']]); if (!$user) { - $user = new User(); - $added = true; - } else { - $added = false; + $user = new User(); } if (array_key_exists('teamid', $accountItem['user'])) { @@ -1185,12 +1373,11 @@ protected function importAccountData(array $accountData, ?array &$saved = null): unset($accountItem['user']['teamid']); $team = null; if ($teamId !== null) { - $field = $this->eventLogService->apiIdFieldForEntity(Team::class); - $team = $this->em->getRepository(Team::class)->findOneBy([$field => $teamId]); + $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamId]); if (!$team) { $team = new Team(); $team - ->setExternalid($teamId) + ->setExternalid((string)$teamId) ->setName($teamId . ' - auto-create during import'); $this->em->persist($team); $this->dj->auditlog('team', $team->getTeamid(), @@ -1205,15 +1392,41 @@ protected function importAccountData(array $accountData, ?array &$saved = null): $propertyAccessor->setValue($user, $field, $value); } - if ($added) { - $this->em->persist($user); - } - $this->em->flush(); + $errors = $this->validator->validate($user); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } - if ($saved !== null) { - $saved[] = $user; + $message .= sprintf("User at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $allUsers[] = $user; + + if ($saved !== null) { + $saved[] = $user; + } } + } + + if ($anyErrors) { + return 0; + } + foreach ($allUsers as $user) { + $this->em->persist($user); + } + + foreach ($newTeams as $newTeam) { + $team = $newTeam['team']; + $this->em->persist($team); + } + + $this->em->flush(); + + foreach ($allUsers as $user) { $this->dj->auditlog('user', $user->getUserid(), 'replaced', 'imported from tsv'); } @@ -1221,6 +1434,8 @@ protected function importAccountData(array $accountData, ?array &$saved = null): foreach ($newTeams as $newTeam) { $team = $newTeam['team']; $action = $newTeam['action']; + $this->dj->auditlog('team', $team->getTeamid(), 'replaced', + 'imported from tsv, autocreated for judge'); $this->eventLogService->log('team', $team->getTeamid(), $action, $contest->getCid()); } } @@ -1287,8 +1502,7 @@ protected function importAccountsTsv(array $content, ?string &$message = null): $line[2]); return -1; } - $field = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $team = $this->em->getRepository(Team::class)->findOneBy([$field => $teamId]); + $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamId]); if ($team === null) { $message = sprintf('Unknown team id %s on line %d', $teamId, $lineNr); return -1; diff --git a/webapp/src/Service/ImportProblemService.php b/webapp/src/Service/ImportProblemService.php index 10f77ea328b..8b58db4f46c 100644 --- a/webapp/src/Service/ImportProblemService.php +++ b/webapp/src/Service/ImportProblemService.php @@ -9,7 +9,7 @@ use App\Entity\Problem; use App\Entity\ProblemAttachment; use App\Entity\ProblemAttachmentContent; -use App\Entity\ProblemTextContent; +use App\Entity\ProblemStatementContent; use App\Entity\Submission; use App\Entity\Team; use App\Entity\Testcase; @@ -210,8 +210,8 @@ public function importZippedProblem( ->setCombinedRunCompare(false) ->setMemlimit(null) ->setOutputlimit(null) - ->setProblemTextContent(null) - ->setProblemtextType(null); + ->setProblemStatementContent(null) + ->setProblemstatementType(null); $contestProblem ?->setPoints(1) @@ -265,10 +265,6 @@ public function importZippedProblem( $yamlData = Yaml::parse($problemYaml); if (!empty($yamlData)) { - if (isset($yamlData['uuid']) && $contestProblem !== null) { - $contestProblem->setShortname($yamlData['uuid']); - } - $yamlProblemProperties = []; if (isset($yamlData['name'])) { if (is_array($yamlData['name'])) { @@ -314,11 +310,11 @@ public function importZippedProblem( $filename = sprintf('%sproblem.%s', $dir, $type); $text = $zip->getFromName($filename); if ($text !== false) { - $content = (new ProblemTextContent()) + $content = (new ProblemStatementContent()) ->setContent($text); $problem - ->setProblemTextContent($content) - ->setProblemtextType($type); + ->setProblemStatementContent($content) + ->setProblemstatementType($type); $messages['info'][] = "Added/updated problem statement from: $filename"; break 2; } @@ -870,7 +866,7 @@ public function importProblemFromRequest(Request $request, ?int $contestId = nul $problem = $this->em->createQueryBuilder() ->from(Problem::class, 'p') ->select('p') - ->andWhere(sprintf('p.%s = :id', $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid')) + ->andWhere('p.externalid = :id') ->setParameter('id', $probId) ->getQuery() ->getOneOrNullResult(); @@ -890,7 +886,7 @@ public function importProblemFromRequest(Request $request, ?int $contestId = nul $allMessages = array_merge($allMessages, $messages); if ($newProblem) { $this->dj->auditlog('problem', $newProblem->getProbid(), 'upload zip', $clientName); - $probId = $newProblem->getApiId($this->eventLogService); + $probId = $newProblem->getExternalid(); } else { $errors = array_merge($errors, $messages); } diff --git a/webapp/src/Service/RejudgingService.php b/webapp/src/Service/RejudgingService.php index 5cb0f8bf75b..b047add6fb6 100644 --- a/webapp/src/Service/RejudgingService.php +++ b/webapp/src/Service/RejudgingService.php @@ -82,13 +82,23 @@ public function createRejudging( $index = 0; $first = true; foreach ($judgings as $judging) { + $submission = $judging->getSubmission(); + $contestProblem = $submission->getContestProblem(); + $language = $submission->getLanguage(); + $index++; - if ($judging->getSubmission()->getRejudging() !== null) { - // The submission is already part of another rejudging, record and skip it. + if ( + // Record and skip submission/judging if it is already part of another judging or is not allowed + // to be judged. + $submission->getRejudging() !== null + || !$contestProblem->getAllowJudge() + || !$language->getAllowJudge() + ) { $skipped[] = $judging; continue; } + $this->em->wrapInTransaction(function () use ( $priority, $singleJudging, @@ -277,9 +287,10 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable } // Update balloons. - $contest = $this->em->getRepository(Contest::class)->find($submission['cid']); - $submission = $this->em->getRepository(Submission::class)->find($submission['submitid']); - $this->balloonService->updateBalloons($contest, $submission); + $contest = $this->em->getRepository(Contest::class)->find($submission['cid']); + $submissionEntity = $this->em->getRepository(Submission::class)->find($submission['submitid']); + $judging = $this->em->getRepository(Judging::class)->find($submission['judgingid']); + $this->balloonService->updateBalloons($contest, $submissionEntity, $judging); }); } elseif ($action === self::ACTION_CANCEL) { // Reset submission and invalidate judging tasks. @@ -379,31 +390,27 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable public function calculateTodo(Rejudging $rejudging): array { // Make sure we have the most recent data. This is necessary to - // guarantee that repeated rejugdings are scheduled correctly. + // guarantee that repeated rejudgings are scheduled correctly. $this->em->flush(); - $todo = $this->em->createQueryBuilder() - ->from(Submission::class, 's') - ->select('COUNT(s)') - ->andWhere('s.rejudging = :rejudging') - ->setParameter('rejudging', $rejudging) - ->getQuery() - ->getSingleScalarResult(); - - $done = $this->em->createQueryBuilder() + $queryBuilder = $this->em->createQueryBuilder() ->from(Judging::class, 'j') ->select('COUNT(j)') ->andWhere('j.rejudging = :rejudging') + ->setParameter('rejudging', $rejudging); + + $clonedQueryBuilder = clone $queryBuilder; + + $todo = $queryBuilder + ->andWhere('j.endtime IS NULL') + ->getQuery() + ->getSingleScalarResult(); + + $done = $clonedQueryBuilder ->andWhere('j.endtime IS NOT NULL') - // This is necessary for rejudgings which apply automatically. - // We remove the association of the submission with the rejudging, - // but not the one of the judging with the rejudging for accounting reasons. - ->andWhere('j.valid = 0') - ->setParameter('rejudging', $rejudging) ->getQuery() ->getSingleScalarResult(); - $todo -= $done; return ['todo' => $todo, 'done' => $done]; } } diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index 4bad3a1db82..e6f2ea208b6 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -41,7 +41,6 @@ public function __construct( protected readonly DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly LoggerInterface $logger, - protected readonly EventLogService $eventLogService ) {} /** @@ -58,12 +57,13 @@ public function getScoreboard( Contest $contest, bool $jury = false, ?Filter $filter = null, - bool $visibleOnly = false + bool $visibleOnly = false, + bool $forceUnfrozen = false, ): ?Scoreboard { $freezeData = new FreezeData($contest); // Don't leak information before start of contest. - if (!$freezeData->started() && !$jury) { + if (!$freezeData->started() && !$jury && !$forceUnfrozen) { return null; } @@ -74,9 +74,9 @@ public function getScoreboard( return new Scoreboard( $contest, $teams, $categories, $problems, - $scoreCache, $freezeData, $jury, + $scoreCache, $freezeData, $jury || $forceUnfrozen, (int)$this->config->get('penalty_time'), - (bool)$this->config->get('score_in_seconds') + (bool)$this->config->get('score_in_seconds'), ); } @@ -280,7 +280,7 @@ public function calculateScoreRow( } // Determine whether we will use external judgements instead of judgings. - $useExternalJudgements = $this->config->get('data_source') == DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; + $useExternalJudgements = $this->dj->shadowMode(); // Note the clause 's.submittime < c.endtime': this is used to // filter out TOO-LATE submissions from pending, but it also means @@ -777,7 +777,7 @@ public function getGroupedAffiliations(Contest $contest): array foreach ($category->getTeams() as $team) { if ($teamaffil = $team->getAffiliation()) { $affiliations[$teamaffil->getName()] = [ - 'id' => $teamaffil->getApiId($this->eventLogService), + 'id' => $teamaffil->getExternalid(), 'name' => $teamaffil->getName(), ]; } @@ -891,7 +891,7 @@ public function getScoreboardTwigData( ], 'static' => $static, ]; - if ($static && $contest && $contest->getFreezeData()->showFinal()) { + if ($static && $contest && ($forceUnfrozen || $contest->getFreezeData()->showFinal())) { unset($data['refresh']); $data['refreshstop'] = true; } @@ -903,14 +903,24 @@ public function getScoreboardTwigData( $scoreFilter = null; } if ($scoreboard === null) { - $scoreboard = $this->getScoreboard($contest, $jury, $scoreFilter); + $scoreboard = $this->getScoreboard( + contest: $contest, + jury: $jury, + filter: $scoreFilter, + forceUnfrozen: $forceUnfrozen + ); } if ($forceUnfrozen) { $scoreboard->getFreezeData() ->setForceValue(FreezeData::KEY_SHOW_FROZEN, false) ->setForceValue(FreezeData::KEY_SHOW_FINAL, true) + ->setForceValue(FreezeData::KEY_SHOW_FINAL_JURY, true) ->setForceValue(FreezeData::KEY_FINALIZED, true); + + if (!$contest->getFinalizetime()) { + $contest->setFinalizetime(Utils::now()); + } } $data['contest'] = $contest; diff --git a/webapp/src/Service/StatisticsService.php b/webapp/src/Service/StatisticsService.php index 76a52a54571..15ff41ce326 100644 --- a/webapp/src/Service/StatisticsService.php +++ b/webapp/src/Service/StatisticsService.php @@ -5,6 +5,7 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Judging; +use App\Entity\Language; use App\Entity\Problem; use App\Entity\Submission; use App\Entity\Team; @@ -58,6 +59,7 @@ public function getTeams(Contest $contest, string $filter): array ->join('t.category', 'tc') ->leftJoin('t.affiliation', 'a') ->join('t.submissions', 'ts') + ->join('ts.language', 'l') ->join('ts.judgings', 'j') ->andWhere('j.valid = true') ->join('ts.language', 'lang') @@ -72,6 +74,7 @@ public function getTeams(Contest $contest, string $filter): array ->join('t.category', 'tc') ->leftJoin('tc.contests', 'cc') ->join('t.submissions', 'ts') + ->join('ts.language', 'l') ->join('ts.judgings', 'j') ->andWhere('j.valid = true') ->join('ts.language', 'lang') @@ -514,6 +517,118 @@ public function getGroupedProblemsStats( return $stats; } + /** + * @return array{ + * contest: Contest, + * problems: ContestProblem[], + * filters: array, + * view: string, + * languages: array, + * team_count: int, + * solved: int, + * not_solved: int, + * total: int, + * problems_solved: array, + * problems_solved_count: int, + * problems_attempted: array, + * problems_attempted_count: int, + * }> + * } + */ + public function getLanguagesStats(Contest $contest, string $view): array + { + /** @var Language[] $languages */ + $languages = $this->em->getRepository(Language::class) + ->createQueryBuilder('l') + ->andWhere('l.allowSubmit = 1') + ->orderBy('l.name') + ->getQuery() + ->getResult(); + + $languageStats = []; + + foreach ($languages as $language) { + $languageStats[$language->getLangid()] = [ + 'name' => $language->getName(), + 'teams' => [], + 'team_count' => 0, + 'solved' => 0, + 'not_solved' => 0, + 'total' => 0, + 'problems_solved' => [], + 'problems_solved_count' => 0, + 'problems_attempted' => [], + 'problems_attempted_count' => 0, + ]; + } + + $teams = $this->getTeams($contest, $view); + foreach ($teams as $team) { + foreach ($team->getSubmissions() as $s) { + if ($s->getContest() != $contest) { + continue; + } + if ($s->getSubmitTime() > $contest->getEndTime()) { + continue; + } + if ($s->getSubmitTime() < $contest->getStartTime()) { + continue; + } + if ($s->getSubmittime() > $contest->getFreezetime()) { + continue; + } + + $language = $s->getLanguage(); + + if (!isset($languageStats[$language->getLangid()]['teams'][$team->getTeamid()])) { + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()] = [ + 'team' => $team, + 'solved' => 0, + 'total' => 0, + ]; + } + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['total']++; + $languageStats[$language->getLangid()]['total']++; + if ($s->getResult() === 'correct') { + $languageStats[$language->getLangid()]['solved']++; + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['solved']++; + $languageStats[$language->getLangid()]['problems_solved'][$s->getProblem()->getProbId()] = $s->getContestProblem(); + } else { + $languageStats[$language->getLangid()]['not_solved']++; + } + $languageStats[$language->getLangid()]['problems_attempted'][$s->getProblem()->getProbId()] = $s->getContestProblem(); + } + } + + foreach ($languageStats as &$languageStat) { + usort($languageStat['teams'], static function (array $a, array $b): int { + if ($a['solved'] === $b['solved']) { + return $b['total'] <=> $a['total']; + } + + return $b['solved'] <=> $a['solved']; + }); + $languageStat['team_count'] = count($languageStat['teams']); + $languageStat['problems_solved_count'] = count($languageStat['problems_solved']); + $languageStat['problems_attempted_count'] = count($languageStat['problems_attempted']); + } + unset($languageStat); + + return [ + 'contest' => $contest, + 'problems' => $this->getContestProblems($contest), + 'filters' => StatisticsService::FILTERS, + 'view' => $view, + 'languages' => $languageStats, + ]; + } + /** * Apply the filter to the given query builder. */ diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 43453b3e1a0..bb7ff306d4e 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -235,8 +235,7 @@ public function getSubmissionList( } } - if ($this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL) { + if ($this->dj->shadowMode()) { // When we are shadow, also load the external results $queryBuilder ->leftJoin('s.external_judgements', 'ej', Join::WITH, 'ej.valid = 1') @@ -556,7 +555,9 @@ public function submitSolution( // First look up any expected results in all submission files to minimize the // SQL transaction time below. - if ($this->dj->checkrole('jury')) { + // Only do this for problem import submissions, as we do not want this for re-submitted submissions nor + // submissions that come through the API, e.g. when doing a replay of an old contest. + if ($this->dj->checkrole('jury') && $source == 'problem import') { $results = null; foreach ($files as $file) { $fileResult = self::getExpectedResults(file_get_contents($file->getRealPath()), @@ -613,6 +614,7 @@ public function submitSolution( $judging ->setContest($contest) ->setSubmission($submission); + $submission->addJudging($judging); if ($juryMember !== null) { $judging->setJuryMember($juryMember); } diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index fb8dfd965df..e4f30664b42 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -8,9 +8,11 @@ use App\Entity\ExternalJudgement; use App\Entity\ExternalRun; use App\Entity\ExternalSourceWarning; +use App\Entity\HasExternalIdInterface; use App\Entity\Judging; use App\Entity\JudgingRun; use App\Entity\Language; +use App\Entity\Problem; use App\Entity\Submission; use App\Entity\SubmissionFile; use App\Entity\TeamCategory; @@ -58,9 +60,9 @@ public function getFunctions(): array return [ new TwigFunction('button', $this->button(...), ['is_safe' => ['html']]), new TwigFunction('calculatePenaltyTime', $this->calculatePenaltyTime(...)), - new TwigFunction('showExternalId', $this->showExternalId(...)), new TwigFunction('customAssetFiles', $this->customAssetFiles(...)), new TwigFunction('globalBannerAssetPath', $this->dj->globalBannerAssetPath(...)), + new TwigFunction('shadowMode', $this->shadowMode(...)), ]; } @@ -107,6 +109,7 @@ public function getFilters(): array new TwigFilter('tsvField', $this->toTsvField(...)), new TwigFilter('fileTypeIcon', $this->fileTypeIcon(...)), new TwigFilter('problemBadge', $this->problemBadge(...), ['is_safe' => ['html']]), + new TwigFilter('problemBadgeForContest', $this->problemBadgeForContest(...), ['is_safe' => ['html']]), new TwigFilter('printMetadata', $this->printMetadata(...), ['is_safe' => ['html']]), new TwigFilter('printWarningContent', $this->printWarningContent(...), ['is_safe' => ['html']]), new TwigFilter('entityIdBadge', $this->entityIdBadge(...), ['is_safe' => ['html']]), @@ -151,9 +154,10 @@ public function getGlobals(): array ), 'show_shadow_differences' => $this->tokenStorage->getToken() && $this->authorizationChecker->isGranted('ROLE_ADMIN') && - $this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + $this->dj->shadowMode(), 'doc_links' => $this->dj->getDocLinks(), 'allow_registration' => $selfRegistrationCategoriesCount !== 0, + 'enable_ranking' => $this->config->get('enable_ranking'), ]; } @@ -212,26 +216,31 @@ public function printtime(string|float|null $datetime, ?string $format = null, ? } } - public function printHumanTimeDiff(float|null $datetime): string + public function printHumanTimeDiff(float|null $startTime = null, float|null $endTime = null): string { - if ($datetime === null) { + if ($startTime === null) { return ''; } - $diff = Utils::now() - $datetime; + $suffix = ''; + if ($endTime === null) { + $suffix = ' ago'; + $endTime = Utils::now(); + } + $diff = $endTime - $startTime; if ($diff < 120) { - return (int)($diff) . ' seconds ago'; + return (int)($diff) . ' seconds' . $suffix; } $diff /= 60; if ($diff < 120) { - return (int)($diff) . ' minutes ago'; + return (int)($diff) . ' minutes' . $suffix; } $diff /= 60; if ($diff < 48) { - return (int)($diff) . ' hours ago'; + return (int)($diff) . ' hours' . $suffix; } $diff /= 24; - return (int)($diff) . ' days ago'; + return (int)($diff) . ' days' . $suffix; } /** @@ -366,7 +375,7 @@ public function testcaseResults(Submission $submission, ?bool $showExternal = fa $externalJudgementId = $externalJudgement?->getExtjudgementid(); $probId = $submission->getProblem()->getProbid(); $testcases = $this->em->getConnection()->fetchAllAssociative( - 'SELECT er.result as runresult, t.ranknumber, t.description + 'SELECT er.result as runresult, t.ranknumber, t.description, t.sample FROM testcase t LEFT JOIN external_run er ON (er.testcaseid = t.testcaseid AND er.extjudgementid = :extjudgementid) @@ -380,7 +389,7 @@ public function testcaseResults(Submission $submission, ?bool $showExternal = fa $judgingId = $judging ? $judging->getJudgingid() : null; $probId = $submission->getProblem()->getProbid(); $testcases = $this->em->getConnection()->fetchAllAssociative( - 'SELECT r.runresult, jh.hostname, jt.valid, t.ranknumber, t.description + 'SELECT r.runresult, jh.hostname, jt.valid, t.ranknumber, t.description, t.sample FROM testcase t LEFT JOIN judging_run r ON (r.testcaseid = t.testcaseid AND r.judgingid = :judgingid) @@ -393,7 +402,12 @@ public function testcaseResults(Submission $submission, ?bool $showExternal = fa } $results = ''; + $lastTypeSample = true; foreach ($testcases as $key => $testcase) { + if ($testcase['sample'] != $lastTypeSample) { + $results .= ' | '; + $lastTypeSample = $testcase['sample']; + } $class = $submissionDone ? 'secondary' : 'primary'; $text = '?'; @@ -572,16 +586,9 @@ public function externalCcsUrl(Submission $submission): ?string { $extCcsUrl = $this->config->get('external_ccs_submission_url'); if (!empty($extCcsUrl)) { - $dataSource = $this->config->get('data_source'); - if ($dataSource == 2 && $submission->getExternalid()) { - return str_replace(['[contest]', '[id]'], - [$submission->getContest()->getExternalid(), $submission->getExternalid()], - $extCcsUrl); - } elseif ($dataSource == 1) { - return str_replace(['[contest]', '[id]'], - [$submission->getContest()->getExternalid(), $submission->getSubmitid()], - $extCcsUrl); - } + return str_replace(['[contest]', '[id]'], + [$submission->getContest()->getExternalid(), $submission->getExternalid()], + $extCcsUrl); } return null; @@ -659,6 +666,7 @@ private function getCommonPrefix(array $strings): string */ public function printHosts(array $hostnames): string { + $hostnames = array_values($hostnames); if (empty($hostnames)) { return ""; } @@ -999,12 +1007,9 @@ public function descriptionExpand(?string $description = null): string } } - /** - * @param object|string $entity - */ - public function showExternalId($entity): bool + public function shadowMode(): bool { - return $this->eventLogService->externalIdFieldForEntity($entity) !== null; + return $this->dj->shadowMode(); } public function wrapUnquoted(string $text, int $width = 75, string $quote = '>'): string @@ -1056,9 +1061,12 @@ public function fileTypeIcon(string $type): string return 'fas fa-file-' . $iconName; } - public function problemBadge(ContestProblem $problem): string + public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string { $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + if ($grayedOut) { + $rgb = 'whitesmoke'; + } $background = Utils::parseHexColor($rgb); // Pick a border that's a bit darker. @@ -1070,8 +1078,12 @@ public function problemBadge(ContestProblem $problem): string // Pick the foreground text color based on the background color. $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; + if ($grayedOut) { + $foreground = 'silver'; + $border = 'linen'; + } return sprintf( - '%s', + '%s', $rgb, $border, $foreground, @@ -1079,6 +1091,13 @@ public function problemBadge(ContestProblem $problem): string ); } + public function problemBadgeForContest(Problem $problem, ?Contest $contest = null): string + { + $contest ??= $this->dj->getCurrentContest(); + $contestProblem = $contest?->getContestProblem($problem); + return $contestProblem === null ? '' : $this->problemBadge($contestProblem); + } + public function printMetadata(?string $metadata): string { if ($metadata === null) { @@ -1171,14 +1190,17 @@ public function entityIdBadge(BaseApiEntity $entity, string $idPrefix = ''): str $propertyAccessor = PropertyAccess::createPropertyAccessor(); $metadata = $this->em->getClassMetadata($entity::class); $primaryKeyColumn = $metadata->getIdentifierColumnNames()[0]; - $externalIdField = $this->eventLogService->externalIdFieldForEntity($entity); $data = [ 'idPrefix' => $idPrefix, 'id' => $propertyAccessor->getValue($entity, $primaryKeyColumn), - 'externalId' => $externalIdField ? $propertyAccessor->getValue($entity, $externalIdField) : null, + 'externalId' => null, ]; + if ($entity instanceof HasExternalIdInterface) { + $data['externalId'] = $entity->getExternalid(); + } + if ($entity instanceof Team) { $data['label'] = $entity->getLabel(); } diff --git a/webapp/src/Utils/Scoreboard/Scoreboard.php b/webapp/src/Utils/Scoreboard/Scoreboard.php index bb81e6fd849..6e923309617 100644 --- a/webapp/src/Utils/Scoreboard/Scoreboard.php +++ b/webapp/src/Utils/Scoreboard/Scoreboard.php @@ -48,6 +48,14 @@ public function __construct( $this->calculateScoreboard(); } + /** + * @return bool Whether this Scoreboard has restricted access (either a jury member can see, or after unfreeze). + */ + public function hasRestrictedAccess(): bool + { + return $this->restricted; + } + /** * @return Team[] */ diff --git a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php index 76aae33bd24..3267322be78 100644 --- a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php +++ b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php @@ -65,8 +65,9 @@ protected function calculateScoreboard(): void $this->matrix[$scoreRow->getTeam()->getTeamid()][$scoreRow->getProblem()->getProbid()] = new ScoreboardMatrixItem( $scoreRow->getIsCorrect($this->restricted), $scoreRow->getIsCorrect($this->showRestrictedFts) && $scoreRow->getIsFirstToSolve(), - $scoreRow->getSubmissions($this->restricted), - $scoreRow->getPending($this->restricted), + // When public scoreboard is frozen, also show "x + y tries" for jury + $scoreRow->getSubmissions($this->freezeData->showFrozen() ? false : $this->restricted), + $scoreRow->getPending($this->freezeData->showFrozen() ? false : $this->restricted), $scoreRow->getSolveTime($this->restricted), $penalty, $scoreRow->getRuntime($this->restricted) diff --git a/webapp/src/Utils/Utils.php b/webapp/src/Utils/Utils.php index 0478c561733..c1ec4acb9a8 100644 --- a/webapp/src/Utils/Utils.php +++ b/webapp/src/Utils/Utils.php @@ -4,6 +4,7 @@ use DateTime; use Doctrine\Inflector\InflectorFactory; use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * Generic utility class. @@ -429,17 +430,17 @@ public static function printhost(string $hostname, bool $full = false): string } /** - * Print (file) size in human readable format by using B,KB,MB,GB suffixes. - * Input is a integer (the size in bytes), output a string with suffix. + * Print (file) size in human-readable format by using B,KB,MB,GB suffixes. + * Input is an integer (the size in bytes), output a string with suffix. */ public static function printsize(int $size, int $decimals = 1): string { $factor = 1024; - $units = ['B', 'KB', 'MB', 'GB']; - $display = (int)$size; + $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + $display = $size; $exact = true; - for ($i = 0; $i < count($units) && $display > $factor; $i++) { + for ($i = 0; $i < count($units) && $display >= $factor; $i++) { if (((int)$display % $factor)!=0) { $exact = false; } @@ -891,4 +892,61 @@ public static function reindex(array $array, callable $callback): array }); return $reindexed; } + + public static function getTextType(string $clientName, string $realPath): ?string + { + $textType = null; + + if (strrpos($clientName, '.') !== false) { + $ext = substr($clientName, strrpos($clientName, '.') + 1); + if (in_array($ext, ['txt', 'html', 'pdf'])) { + $textType = $ext; + } + } + if (!isset($textType)) { + $finfo = finfo_open(FILEINFO_MIME); + + [$type] = explode('; ', finfo_file($finfo, $realPath)); + + finfo_close($finfo); + + switch ($type) { + case 'application/pdf': + $textType = 'pdf'; + break; + case 'text/html': + $textType = 'html'; + break; + case 'text/plain': + $textType = 'txt'; + break; + } + } + + return $textType; + } + + public static function getTextStreamedResponse( + ?string $textType, + BadRequestHttpException $exceptionMessage, + string $filename, + ?string $text + ): StreamedResponse { + $mimetype = match ($textType) { + 'pdf' => 'application/pdf', + 'html' => 'text/html', + 'txt' => 'text/plain', + default => throw $exceptionMessage, + }; + + $response = new StreamedResponse(); + $response->setCallback(function () use ($text) { + echo $text; + }); + $response->headers->set('Content-Type', sprintf('%s; name="%s"', $mimetype, $filename)); + $response->headers->set('Content-Disposition', sprintf('inline; filename="%s"', $filename)); + $response->headers->set('Content-Length', (string)strlen($text)); + + return $response; + } } diff --git a/symfony.lock b/webapp/symfony.lock similarity index 98% rename from symfony.lock rename to webapp/symfony.lock index 3f00c08f96e..77c6511b574 100644 --- a/symfony.lock +++ b/webapp/symfony.lock @@ -26,16 +26,6 @@ "dflydev/dot-access-data": { "version": "v3.0.1" }, - "doctrine/annotations": { - "version": "2.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "main", - "version": "1.10", - "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" - }, - "files": [] - }, "doctrine/cache": { "version": "v1.8.0" }, @@ -208,6 +198,18 @@ "webapp/config/routes/nelmio_api_doc.yaml" ] }, + "nelmio/cors-bundle": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "webapp/config/packages/nelmio_cors.yaml" + ] + }, "nette/schema": { "version": "v1.2.2" }, diff --git a/webapp/templates/jury/analysis/contest_overview.html.twig b/webapp/templates/jury/analysis/contest_overview.html.twig index 3aeb6de59ca..9a860e4ef1d 100644 --- a/webapp/templates/jury/analysis/contest_overview.html.twig +++ b/webapp/templates/jury/analysis/contest_overview.html.twig @@ -24,6 +24,14 @@ table tr a { color: inherit; } + {% endblock %} {% block content %} @@ -36,7 +44,7 @@ table tr a { Contest Stats
-
Language Stats + + + Details +
diff --git a/webapp/templates/jury/analysis/languages.html.twig b/webapp/templates/jury/analysis/languages.html.twig new file mode 100644 index 00000000000..7cb29236aff --- /dev/null +++ b/webapp/templates/jury/analysis/languages.html.twig @@ -0,0 +1,100 @@ +{% extends "jury/base.html.twig" %} + +{% block title %}Analysis - Languages in {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %} + +{% block content %} +

Language stats

+ {% include 'jury/partials/analysis_filter.html.twig' %} + +
+ {% for langid, language in languages %} +
+
+
+ {{ language.name }} +
+
+ {{ language.team_count }} team{% if language.team_count != 1 %}s{% endif %} + {% if language.team_count > 0 %} +
+ + +
+
+
+ + + + + + + + + + {% for team in language.teams %} + + + + + + + {% endfor %} + +
TeamNumber of solved problems in {{ language.name }}Total attempts in {{ language.name }}
+ + {{ team.team | entityIdBadge('t') }} + + + + {{ team.team.effectiveName }} + + {{ team.solved }}{{ team.total }}
+
+
+ {% endif %} +
+ {{ language.total }} total submission{% if language.total != 1 %}s{% endif %} + for {{ language.problems_attempted_count }} problem{% if language.problems_attempted_count != 1 %}s{% endif %}:
+ {% for problem in problems %} + + {{ problem | problemBadge(language.problems_attempted[problem.probid] is not defined) }} + + {% endfor %} +
+ {{ language.solved }} submission{% if language.solved != 1 %}s{% endif %} solved problems + for {{ language.problems_solved_count }} problem{% if language.problems_solved_count != 1 %}s{% endif %}:
+ {% for problem in problems %} + + {{ problem | problemBadge(language.problems_solved[problem.probid] is not defined) }} + + {% endfor %} +
+ {{ language.not_solved }} submission{% if language.not_solved != 1 %}s{% endif %} did not solve a problem
+
+
+
+ {% endfor %} +
+{% endblock %} + +{% block extrafooter %} + +{% endblock %} diff --git a/webapp/templates/jury/base.html.twig b/webapp/templates/jury/base.html.twig index 10a61dfc317..a2e724d5113 100644 --- a/webapp/templates/jury/base.html.twig +++ b/webapp/templates/jury/base.html.twig @@ -34,10 +34,54 @@ } } + $('#keys_disable').click(disableKeys); + $('#keys_enable').click(enableKeys); + var keysCookie = getCookie('domjudge_keys'); + if (keysCookie != 1 && keysCookie != "") { + $('#keys_enable').removeClass('d-none'); + } else { + $('#keys_disable').removeClass('d-none'); + } + updateMenuAlerts(); setInterval(updateMenuAlerts, 20000); $('[data-bs-toggle="tooltip"]').tooltip(); }); + + initializeKeyboardShortcuts(); + +
+

Keyboard shortcuts

+ + ? display this help, Escape to exit
+
+ + j go to the next item, e.g. next submission
+ k go to the previous item, e.g. previous submission
+
+ + s ↵ open the list of submissions
+ s [0-9]+ ↵ open a specific submission, e.g. s42↵ to go to submission 42
+
+ + t ↵ open the list of teams
+ t [0-9]+ ↵ open to a specific team
+
+ + p ↵ open the list of problems
+ p [0-9]+ ↵ open a specific problem
+
+ + c ↵ open the list of clarifications
+ c [0-9]+ ↵ open a specific clarification
+
+ + Shift + j [0-9]+ ↵ open a specific judging
+
+ + Shift + s open the scoreboard
+
+
{% endblock %} diff --git a/webapp/templates/jury/clarification.html.twig b/webapp/templates/jury/clarification.html.twig index 01b1ad80ab0..c1c50d768bc 100644 --- a/webapp/templates/jury/clarification.html.twig +++ b/webapp/templates/jury/clarification.html.twig @@ -24,7 +24,7 @@
Clarification {{ clar.clarid }} - {% if showExternalId %} + {% if shadowMode() %} (external ID: {{ clar.externalid }}) {% endif %}
@@ -159,7 +159,7 @@
-
+
Send clarification
diff --git a/webapp/templates/jury/clarifications.html.twig b/webapp/templates/jury/clarifications.html.twig index 75d4414fd27..411917f6685 100644 --- a/webapp/templates/jury/clarifications.html.twig +++ b/webapp/templates/jury/clarifications.html.twig @@ -23,9 +23,12 @@ {%- else %}
diff --git a/webapp/templates/jury/config.html.twig b/webapp/templates/jury/config.html.twig index adfaf9767e1..d9f41c83572 100644 --- a/webapp/templates/jury/config.html.twig +++ b/webapp/templates/jury/config.html.twig @@ -15,6 +15,28 @@ {% endblock %} {% block content %} + {% if diffs is not null %} + {% if diffs %} + + {% else %} + + {% endif %} + + {% endif %}

Configuration

diff --git a/webapp/templates/jury/contest.html.twig b/webapp/templates/jury/contest.html.twig index 7a69b58aa69..a62eb9c3f1b 100644 --- a/webapp/templates/jury/contest.html.twig +++ b/webapp/templates/jury/contest.html.twig @@ -40,90 +40,85 @@ CID c{{ contest.cid }} + - {% if showExternalId(contest) %} - - External ID - {{ contest.externalid }} - - {% endif %} - Short name - {{ contest.shortname }} + External ID + {{ contest.externalid }} + - Activate time - - {{ contest.activatetimeString }} - {% if contest.isActive %} - - {% endif %} - + Short name + {{ contest.shortname }} + - - Start time - - {% if contest.starttimeEnabled %} - {{ contest.starttimeString }} - {% if contest.state.started %} - + {% for type, data in contest.dataForJuryInterface %} + + {{ data.label }}: + + {{ data.time }} + {% if data.icon is defined %} + {% endif %} + + {% if is_granted('ROLE_ADMIN') %} + + {% if data.show_button %} + {% set button_label = type ~ " now" %} + {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': type}), button_label, 'primary btn-sm timebutton') }} + {% endif %} + {% if data.extra_button is defined %} + {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': data.extra_button.type}), data.extra_button.label, 'primary btn-sm timebutton') }} + {% endif %} + {% if type == 'finalize' %} + {% if contest.finalizetime %} + {{ button(path('jury_contest_finalize', {'contestId': contest.cid}), 'Update finalization', 'secondary btn-sm timebutton') }} + {% endif %} + {% endif %} + {% else %} - {{ contest.starttimeString }} delayed + {% endif %} - - + + {% endfor %} - Scoreboard freeze + Allow submit - {{ contest.freezetimeString|default('-') }} - {% if contest.state.frozen %} - - {% endif %} + {% include 'jury/partials/contest_toggle.html.twig' with {type: 'submit', enabled: contest.allowSubmit} %} + + {% if contest.contestProblemsetType is not empty %} + + Problemset document + + + + + + + + {% endif %} - End time + Process balloons - {{ contest.endtimeString }} - {% if contest.state.ended %} - - {% endif %} + {% include 'jury/partials/contest_toggle.html.twig' with {type: 'balloons', enabled: contest.processBalloons} %} + - Scoreboard unfreeze + Runtime as tiebreaker - {{ contest.unfreezetimeString|default('-') }} - {% if contest.state.thawed %} - - {% endif %} + {% include 'jury/partials/contest_toggle.html.twig' with {type: 'tiebreaker', enabled: contest.runtimeAsScoreTiebreaker} %} - Deactivate time - {{ contest.deactivatetimeString }} - - - Allow submit + Process medals - - -
+ {% include 'jury/partials/contest_toggle.html.twig' with {type: 'medals', enabled: contest.medalsEnabled} %} - - - Process balloons - {{ contest.processBalloons | printYesNo }} - - - Runtime as tiebreaker - {{ contest.runtimeAsScoreTiebreaker | printYesNo }} - - - Process medals - {{ contest.medalsEnabled | printYesNo }} + Medals @@ -161,14 +156,19 @@ none {% endif %} + Publicly visible - {{ contest.public | printYesNo }} + + {% include 'jury/partials/contest_toggle.html.twig' with {type: 'public', enabled: contest.public} %} + + Open to all teams {{ contest.openToAllTeams | printYesNo }} + Teams @@ -191,6 +191,7 @@ {% endfor %} {% endif %} + Public static scoreboard ZIP @@ -199,6 +200,7 @@ Download + Jury (unfrozen) static scoreboard ZIP @@ -207,6 +209,7 @@ Download + Sample data ZIP @@ -219,11 +222,9 @@ Contains samples, attachments and statement for all problems. + - {% set contestId = contest.cid %} - {% if showExternalId(contest) %} - {% set contestId = contest.externalid %} - {% endif %} + {% set contestId = contest.externalid %} {% set banner = contestId | assetPath('contest') %} {% if not banner %} {% set banner = globalBannerAssetPath() %} @@ -232,10 +233,13 @@ Banner + + {% endif %} Warning message {{ contest.warningMessage }} +
@@ -377,8 +381,12 @@ {{ problem.problem.name }} {{ problem.shortname }} {{ problem.points }} - {{ problem.allowSubmit | printYesNo }} - {{ problem.allowJudge | printYesNo }} + + {% include 'jury/partials/problem_toggle.html.twig' with {contestProblem: problem, type: 'submit', enabled: problem.allowSubmit} %} + + + {% include 'jury/partials/problem_toggle.html.twig' with {contestProblem: problem, type: 'judge', enabled: problem.allowJudge} %} + {% if problem.color is empty %}   {% else %} @@ -394,9 +402,9 @@ - {% if problem.problem.problemtextType %} - - + {% if problem.problem.problemstatementType %} + + {% endif %} @@ -431,11 +439,6 @@ {{ button(path('jury_contest_edit', {'contestId': contest.cid}), 'Edit', 'primary', 'edit') }} {{ button(path('jury_contest_delete', {'contestId': contest.cid}), 'Delete', 'danger', 'trash-alt', true) }} {{ button(path('jury_contest_lock', {'contestId': contest.cid}), 'Lock', 'secondary', 'lock') }} - {% if contest.finalizetime %} - {{ button(path('jury_contest_finalize', {'contestId': contest.cid}), 'Update finalization', 'secondary', 'lock') }} - {% else %} - {{ button(path('jury_contest_finalize', {'contestId': contest.cid}), 'Finalize this contest', 'secondary', 'flag-checkered') }} - {% endif %} {% endif %} {{ button(path('jury_contest_request_remaining', {'contestId': contest.cid}), 'Judge remaining testcases', 'secondary', 'gavel') }} {% endif %} diff --git a/webapp/templates/jury/contests.html.twig b/webapp/templates/jury/contests.html.twig index 04cc260fc86..484721ceeae 100644 --- a/webapp/templates/jury/contests.html.twig +++ b/webapp/templates/jury/contests.html.twig @@ -6,6 +6,7 @@ {% block extrahead %} {{ parent() }} {{ macros.table_extrahead() }} + {{ macros.toggle_extrahead() }} {% endblock %} {% block content %} @@ -14,57 +15,51 @@

Current contests

{% for contest in current_contests %} - {# TODO: at some point use real Symfony forms here? Is maybe hard because of all the submit buttons... #} -
- -
-
-
-
- {{ contest.name }} ({{ contest.shortname }} - c{{ contest.cid }}) - {% if contest.locked %} - - {% endif %} -
-
- {% if not contest.starttimeEnabled and contest.finalizetime is not empty %} -
- Warning: start time is undefined, but contest is finalized! -
- {% endif %} - - - {% for type, data in contest.dataForJuryInterface %} - - + {% endfor %} + +
- {% if data.icon is defined %} - +
+
+
+
+ {{ contest.name }} ({{ contest.shortname }} - c{{ contest.cid }}) + {% if contest.locked %} + + {% endif %} +
+
+ {% if not contest.starttimeEnabled and contest.finalizetime is not empty %} +
+ Warning: start time is undefined, but contest is finalized! +
+ {% endif %} + + + {% for type, data in contest.dataForJuryInterface %} + + + + + {% if is_granted('ROLE_ADMIN') %} + - - - {% if is_granted('ROLE_ADMIN') %} - - {% endif %} - - {% endfor %} - -
+ {% if data.icon is defined %} + + {% endif %} + {{ data.label }}:{{ data.time }} + {% if data.show_button %} + {% set button_label = type ~ " now" %} + {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': type}), button_label, 'primary btn-sm') }} + {% endif %} + {% if data.extra_button is defined %} + {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': data.extra_button.type}), data.extra_button.label, 'primary btn-sm') }} {% endif %} {{ data.label }}:{{ data.time }} - {% if data.show_button %} - - {% endif %} - {% if data.extra_button is defined %} - - {% endif %} -
-
+ {% endif %} +
- +
{% else %} {% if upcoming_contest is empty %}
@@ -77,10 +72,7 @@ {{ upcoming_contest.name }} ({{ upcoming_contest.shortname }}); active from {{ upcoming_contest.activatetime | printtime('D d M Y H:i:s T') }}

-
- - -
+ {{ button(path('jury_contest_donow', {'contestId': upcoming_contest.cid, 'time': 'activate'}), 'Activate now', 'primary') }}
{% endif %} {% endfor %} @@ -97,3 +89,8 @@ {% endif %} {% endblock %} + +{% block extrafooter %} + {{ macros.toggle_autosubmit_extrafooter() }} +{% endblock %} + diff --git a/webapp/templates/jury/entity_id_badge.html.twig b/webapp/templates/jury/entity_id_badge.html.twig index 037714630ab..8e304dc7485 100644 --- a/webapp/templates/jury/entity_id_badge.html.twig +++ b/webapp/templates/jury/entity_id_badge.html.twig @@ -1,4 +1,4 @@ - + {% if label is defined and label | length %} {{ label }} {% elseif externalId is not null %} diff --git a/webapp/templates/jury/executable.html.twig b/webapp/templates/jury/executable.html.twig index 883dde50020..94569d1e4e3 100644 --- a/webapp/templates/jury/executable.html.twig +++ b/webapp/templates/jury/executable.html.twig @@ -48,21 +48,21 @@ {% if executable.type == 'compare' %} {% for problem in executable.problemsCompare %} - p{{ problem.probid }} + p{{ problem.probid }} {{ problem | problemBadgeForContest }} {% set used = true %} {% endfor %} {% elseif executable.type == 'run' %} {% for problem in executable.problemsRun %} - p{{ problem.probid }} + p{{ problem.probid }} {{ problem | problemBadgeForContest }} {% set used = true %} {% endfor %} {% elseif executable.type == 'compile' %} {% for language in executable.languages %} - {{ language.langid }} + {{ language | entityIdBadge }} {% set used = true %} {% endfor %} @@ -135,8 +135,8 @@ {{ form_widget(form) }}
- {{ form_end(form) }} {% endif %} + {{ form_end(form) }} {% if is_granted('ROLE_ADMIN') %}
diff --git a/webapp/templates/jury/export/clarifications.html.twig b/webapp/templates/jury/export/clarifications.html.twig index fccf0b69eb9..0ee58eacb3f 100644 --- a/webapp/templates/jury/export/clarifications.html.twig +++ b/webapp/templates/jury/export/clarifications.html.twig @@ -94,7 +94,7 @@ Content -
{{ clarification.body | wrapUnquoted(80) }}
+
{{ clarification.body | markdown_to_html | sanitize_html('app.clarification_sanitizer') }}
{% if clarification.replies is not empty %} @@ -110,7 +110,7 @@ -
{{ reply.body | wrapUnquoted(80) }}
+
{{ reply.body | markdown_to_html | sanitize_html('app.clarification_sanitizer') }}
{% endfor %} diff --git a/webapp/templates/jury/export/layout.html.twig b/webapp/templates/jury/export/layout.html.twig index 22bf7d84619..4e465a2aab8 100644 --- a/webapp/templates/jury/export/layout.html.twig +++ b/webapp/templates/jury/export/layout.html.twig @@ -82,9 +82,30 @@ padding-top: 2rem; } + code { + font-size: .875em; + color: rgb(214, 51, 132); + word-wrap: break-word; + } + pre { + border-top: 1px dotted #C0C0C0; + border-bottom: 1px dotted #C0C0C0; + background-color: #FAFAFA; margin: 0; - white-space: pre-wrap; + padding: 5px; + font-family: monospace; + white-space: pre; + } + + pre > code { + color: inherit; + } + + blockquote { + border-left: darkgrey solid .2em; + padding-left: .5em; + color: darkgrey; } diff --git a/webapp/templates/jury/export/results.html.twig b/webapp/templates/jury/export/results.html.twig index 9f3d10db52f..3e359ad0b6b 100644 --- a/webapp/templates/jury/export/results.html.twig +++ b/webapp/templates/jury/export/results.html.twig @@ -10,7 +10,7 @@ Award Solved problems Total time - Time of last submission + Time of last accepted submission @@ -27,25 +27,35 @@ -

Other ranked teams

- - - - - - - - - - {% for row in ranked %} - - - - - - {% endfor %} - -
RankTeamSolved problems
{{ row.rank }}{{ row.team }}{{ row.solved }}
+ {% for award in ['Ranked', 'Highest Honors', 'High Honors', 'Honors'] %} + {% if ranked[award] is defined %} +

+ {% if award == 'Ranked' %} + Other ranked teams + {% else %} + {{ award }} + {% endif %} +

+ + + + + + + + + + {% for row in ranked[award] %} + + + + + + {% endfor %} + +
RankTeamSolved problems
{{ row.rank }}{{ row.team }}{{ row.solved }}
+ {% endif %} + {% endfor %}

Honorable mentions

@@ -66,6 +76,7 @@ + @@ -73,6 +84,7 @@ + {% endfor %} @@ -84,6 +96,7 @@ + @@ -98,6 +111,13 @@ Not solved {% endif %} + {%- endfor %} {%- for action in row.actions %} diff --git a/webapp/templates/jury/language.html.twig b/webapp/templates/jury/language.html.twig index 47b98f41b3a..6991e61b8f1 100644 --- a/webapp/templates/jury/language.html.twig +++ b/webapp/templates/jury/language.html.twig @@ -20,12 +20,10 @@ - {% if showExternalId(language) %} - - - - - {% endif %} + + + + @@ -74,7 +66,7 @@ diff --git a/webapp/templates/jury/languages.html.twig b/webapp/templates/jury/languages.html.twig index 0df29e4c15a..f7c98db478c 100644 --- a/webapp/templates/jury/languages.html.twig +++ b/webapp/templates/jury/languages.html.twig @@ -6,6 +6,7 @@ {% block extrahead %} {{ parent() }} {{ macros.table_extrahead() }} + {{ macros.toggle_extrahead() }} {% endblock %} {% block content %} @@ -28,3 +29,7 @@ {% endif %} {% endblock %} + +{% block extrafooter %} + {{ macros.toggle_autosubmit_extrafooter() }} +{% endblock %} diff --git a/webapp/templates/jury/menu.html.twig b/webapp/templates/jury/menu.html.twig index 29cc607a8d5..bf46240ec65 100644 --- a/webapp/templates/jury/menu.html.twig +++ b/webapp/templates/jury/menu.html.twig @@ -116,19 +116,25 @@ {% if refresh is defined and refresh %} - - {% if refresh_flag %} - Disable Refresh - {% else %} - Enable Refresh - {% endif %} - - {% if refresh %} - ({{ refresh.after }}s) - {% endif %} + + + {% if refresh_flag %} + Disable Refresh + {% else %} + Enable Refresh + {% endif %} + + ({{ refresh.after }}s) {% endif %} + + Disable keyboard shortcuts + + + Enable keyboard shortcuts + + Logout diff --git a/webapp/templates/jury/partials/clarification_list.html.twig b/webapp/templates/jury/partials/clarification_list.html.twig index 337499bb1c9..33ffcbcf3e4 100644 --- a/webapp/templates/jury/partials/clarification_list.html.twig +++ b/webapp/templates/jury/partials/clarification_list.html.twig @@ -3,7 +3,7 @@ - {% if showExternalId %} + {% if shadowMode() %} {% endif %} {%- if current_contest is null and current_contests | length > 1 %} @@ -28,7 +28,7 @@ - {% if showExternalId %} + {% if shadowMode() %} {% endif %} {%- if current_contest is null and current_contests | length > 1 %} @@ -71,7 +71,7 @@ {% if showExternalResult and showExternalTestcases %} - {% if showExternalId(problem) %} - - - - - {% endif %} + + + + - {% if problem.problemtextType is not empty %} + {% if problem.problemstatementType is not empty %} - + @@ -163,10 +162,10 @@ {{ contestProblem.shortname }} {% if contestProblem.color is empty %} @@ -249,3 +248,7 @@ {% endblock %} + +{% block extrafooter %} + {{ macros.toggle_autosubmit_extrafooter() }} +{% endblock %} diff --git a/webapp/templates/jury/problemset.html.twig b/webapp/templates/jury/problemset.html.twig index 50a6daf85b0..cbab45abf58 100644 --- a/webapp/templates/jury/problemset.html.twig +++ b/webapp/templates/jury/problemset.html.twig @@ -1,11 +1,14 @@ {% extends "jury/base.html.twig" %} -{% block title %}Contest problems {{ current_jury_contest.shortname | default('') }} - {{ parent() }}{% endblock %} +{% block title %}Contest problems {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %} {% block content %} {% include 'partials/problem_list.html.twig' with { - contest: current_team_contest, - problem_text_path: 'jury_problem_text', + contest: current_contest, + show_contest_problemset: true, + contest_problemset_path: 'jury_contest_problemset', + contest_problemset_add_cid: true, + problem_statement_path: 'jury_problem_statement', problem_attachment_path: 'jury_attachment_fetch', problem_sample_zip_path: 'jury_problem_sample_zip', show_jury_warning: true diff --git a/webapp/templates/jury/rejudging.html.twig b/webapp/templates/jury/rejudging.html.twig index 34aea20c5a4..2753198c8d2 100644 --- a/webapp/templates/jury/rejudging.html.twig +++ b/webapp/templates/jury/rejudging.html.twig @@ -105,6 +105,27 @@
Judgings in this rejudging will be applied automatically.
{% endif %} + {% if disabledLangs %} +
+ The following languages are currently not allowed to be judged: +
    + {% for id, name in disabledLangs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} + {% if disabledProbs %} +
+ The following problems are currently not allowed to be judged: +
    + {% for id, name in disabledProbs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} +
{% include 'jury/partials/rejudging_matrix.html.twig' %}
diff --git a/webapp/templates/jury/submission.html.twig b/webapp/templates/jury/submission.html.twig index 8dc109b7af4..b965604dced 100644 --- a/webapp/templates/jury/submission.html.twig +++ b/webapp/templates/jury/submission.html.twig @@ -158,19 +158,21 @@ {% endif %} - - - - {{ submission.contest.shortname }} - {{ submission.contest | entityIdBadge('c') }} - - + {% if current_contest.cid != submission.contest.cid %} + + + + {{ submission.contest.shortname }} + {{ submission.contest | entityIdBadge('c') }} + + + {% endif %} {% if submission.contestProblem %} - {{ submission.contestProblem.shortname }}: {{ submission.problem.name }} + {{ submission.contestProblem | problemBadge }}: {{ submission.problem.name }} {% else %} {{ submission.problem.name }} {% endif %} @@ -218,7 +220,7 @@ {% endif %} - {% if submission.externalid %} + {% if shadowMode() and submission.externalid %}
External ID: {% if external_ccs_submission_url is empty %} @@ -341,7 +343,10 @@ {% if selectedJudging is not null or externalJudgement is not null %} - {% include 'jury/partials/submission_graph.html.twig' %} + {% if (selectedJudging is not null and selectedJudging.result != 'compiler-error') + or (externalJudgement is not null and externalJudgement.result != 'compiler-error') %} + {% include 'jury/partials/submission_graph.html.twig' %} + {% endif %} {% if selectedJudging is not null %} @@ -409,7 +414,6 @@
{% if not submission.importError %} - Result: {% if selectedJudging is null or selectedJudging.result is empty %} {%- if selectedJudging and selectedJudging.started %} {{- '' | printValidJuryResult -}} @@ -426,7 +430,7 @@ {%- if lastJudging is not null -%} {% set lastSubmissionLink = path('jury_submission', {submitId: lastSubmission.submitid}) %}{#- -#} - (s{{ lastSubmission.submitid }}: {{ lastJudging.result }}){#- + (s{{ lastSubmission.submitid }}: {{ lastJudging.result | printResult }}){#- -#} {%- endif -%} {%- if externalJudgement is not null %} @@ -437,20 +441,19 @@ {% endif %} {%- endif %} {%- if selectedJudging is not null and judgehosts is not empty -%} - , Judgehost(s): - {% for judgehostid, hostname in judgehosts %} - {% set judgehostLink = path('jury_judgehost', {judgehostid: judgehostid}) %} - {{ hostname | printHost }} - {% endfor %} - - Judging started: {{ selectedJudging.starttime | printtime('H:i:s') }} - {%- if selectedJudging.endtime -%} - , finished in {{ selectedJudging.starttime | printtimediff(selectedJudging.endtime) }}s - {%- elseif selectedJudging.valid or selectedJudging.rejudging -%} + , on {{ judgehosts | printHosts }} + {% if selectedJudging.starttime %} + {% if selectedJudging.endtime %} + , took {{ selectedJudging.starttime | printHumanTimeDiff(selectedJudging.endtime) }} + {% elseif selectedJudging.valid or selectedJudging.rejudging %}  [still judging - busy {{ selectedJudging.starttime | printtimediff }}] - {%- else -%} + {% else %}  [aborted] - {%- endif -%} - + {% endif %} + (started: {{ selectedJudging.starttime | printtime('H:i:s') }}) + {% else %} + , not started yet + {% endif %} {% endif -%} {%- if externalJudgement is not null %} (external judging started: {{ externalJudgement.starttime | printtime('H:i:s') }} @@ -461,21 +464,6 @@ {%- endif -%} ) {%- endif -%} - {%- if selectedJudging is not null and selectedJudging.result != 'compiler-error' -%} - , max/sum runtime: - {{ selectedJudging.maxRuntime | number_format(2, '.', '') }}/{{ selectedJudging.sumRuntime | number_format(2, '.', '') }}s - {%- if lastJudging is not null -%} - - (s{{ lastSubmission.submitid }}: - {{ lastJudging.maxRuntime | number_format(2, '.', '') }}{#- - -#}/{{ lastJudging.sumRuntime | number_format(2, '.', '') }}s) - - {%- endif -%} - {% endif -%} - {%- if externalJudgement is not null and externalJudgement.result != 'compiler-error' and externalJudgement.result != null -%} - , external max/sum runtime: - {{ externalJudgement.maxRuntime | number_format(2, '.', '') }}/{{ externalJudgement.sumRuntime | number_format(2, '.', '') }}s - {% endif %}
{# Display testcase results #} @@ -483,7 +471,6 @@
Region TeamRank
{{ row.group }} {{ row.team }}{{ row.rank }}
Problem TeamRank Time
+ {% if row.rank is not null %} + {{ row.rank }} + {% else %} + - + {% endif %} + {% if row.time is not null %} {{ row.time }} diff --git a/webapp/templates/jury/import_export.html.twig b/webapp/templates/jury/import_export.html.twig index d7be0d78460..0cd8971f40c 100644 --- a/webapp/templates/jury/import_export.html.twig +++ b/webapp/templates/jury/import_export.html.twig @@ -88,74 +88,50 @@

Results

-
-
-
+
-

Export <html>

- +

Export clarifications

+
-
-
-
+
-

Export tab-separated

-
    -
  • - wf_results.tsv: - -
  • -
  • - full_results.tsv: - -
  • -
-
+

Export results

+ {{ form(export_results_form) }} +
{% endblock %} +{% block extrafooter %} + {{ parent() }} + +{% endblock %} diff --git a/webapp/templates/jury/jury_macros.twig b/webapp/templates/jury/jury_macros.twig index 9d8b16fe256..8ee16e64d64 100644 --- a/webapp/templates/jury/jury_macros.twig +++ b/webapp/templates/jury/jury_macros.twig @@ -131,8 +131,10 @@ {%- if item.sortvalue is defined %} data-sort="{{ item.sortvalue }}"{% endif %} {% if (column.render | default('')) == "entity_id_badge" %}style="text-align: right;" {% endif %}> {%- if item.link is defined %} - {%- elseif row.link is defined %}{% endif %} - {% if key == "status" %} + {%- elseif row.link is defined and not item.toggle_partial is defined %}{% endif %} + {% if item.toggle_partial is defined %} + {% include 'jury/partials/' ~ item.toggle_partial with item.partial_arguments %} + {% elseif key == "status" %} {{- (item.value|default(item.default|default('')))|statusIcon -}} {% elseif key == "country" %} {{- (item.value|default(item.default|default('')))|countryFlag -}} @@ -150,7 +152,7 @@ {{- (item.value|default(item.default|default(''))) -}} {% endif %} {% if item.icon is defined %}{%- endif %} - {%- if item.link is defined or row.link is defined -%}{% endif %} + {%- if item.link is defined or (row.link is defined and not item.toggle_partial is defined) -%}{% endif %}
ID {{ language.langid }}
External ID{{ language.externalid }}
External ID{{ language.externalid }}
Entry point @@ -38,19 +36,13 @@
Allow submit -
- -
+ {% include 'jury/partials/language_toggle.html.twig' with {path: 'jury_language_toggle_submit', value: language.allowSubmit} %}
Allow judge -
- -
+ {% include 'jury/partials/language_toggle.html.twig' with {path: 'jury_language_toggle_judge', value: language.allowJudge} %}
Filter files passed to compiler by extension list - {{ language.filterCompilerFiles | printYesNo }} + {% include 'jury/partials/language_toggle.html.twig' with {path: 'jury_language_toggle_filter_compiler_files', value: language.filterCompilerFiles} %}
IDexternal ID
{{ clarification.clarid }}{{ clarification.externalid }} {%- if clarification.problem -%} - problem {{ clarification.problem.contestProblems.first | problemBadge -}} + problem {{ clarification.contestProblem | problemBadge -}} {%- elseif clarification.category -%} {{- categories[clarification.category]|default('general') -}} {%- else -%} diff --git a/webapp/templates/jury/partials/contest_form.html.twig b/webapp/templates/jury/partials/contest_form.html.twig index 83aaf2c9b2b..9e30edaff09 100644 --- a/webapp/templates/jury/partials/contest_form.html.twig +++ b/webapp/templates/jury/partials/contest_form.html.twig @@ -5,9 +5,7 @@ {# These are the errors related to removed intervals #} {{ form_errors(form) }} - {% if form.offsetExists('externalid') %} - {{ form_row(form.externalid) }} - {% endif %} + {{ form_row(form.externalid) }} {{ form_row(form.shortname) }} {{ form_row(form.name) }} {{ form_row(form.activatetimeString) }} @@ -38,6 +36,10 @@ {% if form.offsetExists('clearBanner') %} {{ form_row(form.clearBanner) }} {% endif %} + {{ form_row(form.contestProblemsetFile) }} + {% if form.offsetExists('clearContestProblemset') %} + {{ form_row(form.clearContestProblemset) }} + {% endif %} {{ form_row(form.warningMessage) }}
diff --git a/webapp/templates/jury/partials/contest_toggle.html.twig b/webapp/templates/jury/partials/contest_toggle.html.twig new file mode 100644 index 00000000000..8d23d4b9eaf --- /dev/null +++ b/webapp/templates/jury/partials/contest_toggle.html.twig @@ -0,0 +1,4 @@ +
+ +
diff --git a/webapp/templates/jury/partials/language_toggle.html.twig b/webapp/templates/jury/partials/language_toggle.html.twig new file mode 100644 index 00000000000..1335bd45d26 --- /dev/null +++ b/webapp/templates/jury/partials/language_toggle.html.twig @@ -0,0 +1,4 @@ +
+ +
diff --git a/webapp/templates/jury/partials/problem_toggle.html.twig b/webapp/templates/jury/partials/problem_toggle.html.twig new file mode 100644 index 00000000000..448061b3f76 --- /dev/null +++ b/webapp/templates/jury/partials/problem_toggle.html.twig @@ -0,0 +1,4 @@ +
+ +
diff --git a/webapp/templates/jury/partials/submission_graph.html.twig b/webapp/templates/jury/partials/submission_graph.html.twig index 6a74534f57d..d33220e2a9a 100644 --- a/webapp/templates/jury/partials/submission_graph.html.twig +++ b/webapp/templates/jury/partials/submission_graph.html.twig @@ -1,21 +1,51 @@ {% set timelimit = submission.problem.timelimit * submission.language.timeFactor %}
{% if selectedJudging is not null %} -
-

Testcase Runtimes

+
+
+ testcase CPU times + + {%- if selectedJudging.result != 'compiler-error' -%} + | max: + {{ selectedJudging.maxRuntime | number_format(3, '.', '') }}s + | sum: {{ selectedJudging.sumRuntime | number_format(3, '.', '') }}s + {% endif %} + + +
+
+
{% endif %} {% if externalJudgement is not null and externalJudgement.result is not null %} -
-

External Testcase Runtimes

- +
+
+ External Testcase CPU times + + {%- if externalJudging.result != 'compiler-error' -%} + | max: + {{ externalJudging.maxRuntime | number_format(3, '.', '') }}s + | sum: {{ externalJudging.sumRuntime | number_format(3, '.', '') }}s + {% endif %} + + +
+
+ +
{% endif %} {% if judgings|length > 1 %} -
-

Max Runtimes

- +
+
+
+ Max. CPU times +
+
+ +
+
{% endif %}
@@ -59,7 +89,7 @@ } chart.yAxis .tickValues(tickValues) - .axisLabel('Runtime'); + .axisLabel('CPU time'); if (tickStep >= 1) { chart.yAxis.tickFormat(function(d) { return d3.format(',f')(d) + 's' }); } else { @@ -145,7 +175,7 @@ var format = d3.format(".3f"); return "Testcase " + obj.data.description + "
Runtime: " + format(obj.data.value) + "s"; }); - chart.xAxis.axisLabel("Testcase Rank"); + chart.xAxis.axisLabel("testcase rank"); d3.select('#testcaseruntime svg') .datum(testcase_times) .call(chart); diff --git a/webapp/templates/jury/partials/submission_list.html.twig b/webapp/templates/jury/partials/submission_list.html.twig index a2d6b914c85..b25e24fa1eb 100644 --- a/webapp/templates/jury/partials/submission_list.html.twig +++ b/webapp/templates/jury/partials/submission_list.html.twig @@ -107,7 +107,7 @@
s{{ submission.submitid }} - {% if submission.externalid %} + {% if shadowMode() and submission.externalid %} ({{ submission.externalid }}) {% endif %} @@ -135,7 +135,7 @@ {{ submission.language.langid }} + title="{{ submission.language.name }}">{{ submission.language | entityIdBadge }} diff --git a/webapp/templates/jury/partials/team_category_form.html.twig b/webapp/templates/jury/partials/team_category_form.html.twig index d0b8e67d0a5..b20e6dd7ea6 100644 --- a/webapp/templates/jury/partials/team_category_form.html.twig +++ b/webapp/templates/jury/partials/team_category_form.html.twig @@ -1,9 +1,7 @@
{{ form_start(form) }} - {% if form.offsetExists('externalid') %} - {{ form_row(form.externalid) }} - {% endif %} + {{ form_row(form.externalid) }} {{ form_row(form.icpcid) }} {{ form_row(form.name) }} {{ form_row(form.sortorder) }} diff --git a/webapp/templates/jury/partials/team_form.html.twig b/webapp/templates/jury/partials/team_form.html.twig index 0ad3c087753..81d3f9e5081 100644 --- a/webapp/templates/jury/partials/team_form.html.twig +++ b/webapp/templates/jury/partials/team_form.html.twig @@ -1,9 +1,7 @@
{{ form_start(form) }} - {% if form.offsetExists('externalid') %} - {{ form_row(form.externalid) }} - {% endif %} + {{ form_row(form.externalid) }} {{ form_row(form.icpcid) }} {{ form_row(form.label) }} {{ form_row(form.name) }} diff --git a/webapp/templates/jury/problem.html.twig b/webapp/templates/jury/problem.html.twig index d2407a8b5d6..380054dbf3e 100644 --- a/webapp/templates/jury/problem.html.twig +++ b/webapp/templates/jury/problem.html.twig @@ -6,6 +6,7 @@ {% block extrahead %} {{ parent() }} {{ macros.table_extrahead() }} + {{ macros.toggle_extrahead() }} {% endblock %} {% block content %} @@ -19,12 +20,10 @@
ID p{{ problem.probid }}
External ID{{ problem.externalid }}
External ID{{ problem.externalid }}
Testcases @@ -64,13 +63,13 @@ {% endif %}
Problem textProblem statement - - + +
- {{ contestProblem.allowSubmit | printYesNo }} + {% include 'jury/partials/problem_toggle.html.twig' with {type: 'submit', enabled: contestProblem.allowSubmit} %} - {{ contestProblem.allowJudge | printYesNo }} + {% include 'jury/partials/problem_toggle.html.twig' with {type: 'judge', enabled: contestProblem.allowJudge} %}  
{% if not submission.importError %} - + {% endif %} - {% if lastJudging is not null %} - + {% if externalJudgement is not null %} + {% endif %} - {% if externalJudgement is not null %} - - + {% if lastJudging is not null %} + + {% endif %}
testcase runs: {% if selectedJudging is null %} {% set judgingDone = false %} @@ -491,6 +478,8 @@ {% set judgingDone = selectedJudging.endtime is not empty %} {% endif %} {{ runs | displayTestcaseResults(judgingDone) }} + {% if selectedJudging is not null and runsOutstanding %} {% if selectedJudging.judgeCompletely %} @@ -504,45 +493,40 @@
- s{{ lastSubmission.submitid }} runs: + {{ externalRuns | displayTestcaseResults(externalJudgement.endtime is not empty, true) }} - {{ lastRuns | displayTestcaseResults(lastJudging.endtime is not empty) }} + {% if externalSubmissionUrl and externalSubmissionUrl is not empty %} + + {% endif %} + external {{ externalJudgement.extjudgementid }} + {% if externalSubmissionUrl and externalSubmissionUrl is not empty %} + + {% endif %}
external runs:
- {{ externalRuns | displayTestcaseResults(externalJudgement.endtime is not empty, true) }} + {{ lastRuns | displayTestcaseResults(lastJudging.endtime is not empty) }} + + previous s{{ lastSubmission.submitid }} + {% if lastJudging.verifyComment %} + (verify comment: '{{ lastJudging.verifyComment }}') + {% endif %}
- {# Show JS toggle of previous submission results #} - {% if lastJudging is not null %} - - show/hide - results of previous submission s{{ lastSubmission.submitid }} - {% if lastJudging.verifyComment %} - (verify comment: '{{ lastJudging.verifyComment }}') - {% endif %} - - {% endif %} {% endif %}
- - {# Show verify info, but only when a result is known #} {% if selectedJudging is not null and selectedJudging.result is not empty %} {% include 'jury/partials/verify_form.html.twig' with { @@ -698,13 +682,13 @@ - {% if runsOutput[runIdx].is_output_run_truncated %} + {% if runsOutput[runIdx].is_output_run_truncated_in_db %}
+ {% if disabledLangs %} +
+ The following languages are currently not allowed to be judged: +
    + {% for id, name in disabledLangs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} + {% if disabledProbs %} +
+ The following problems are currently not allowed to be judged: +
    + {% for id, name in disabledProbs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} +
{%- include 'jury/partials/submission_list.html.twig' %}
diff --git a/webapp/templates/jury/team.html.twig b/webapp/templates/jury/team.html.twig index 8c0916d13ae..c52a09ec0da 100644 --- a/webapp/templates/jury/team.html.twig +++ b/webapp/templates/jury/team.html.twig @@ -19,12 +19,10 @@ ID {{ team.teamid }} - {% if showExternalId(team) %} - - External ID - {{ team.externalid }} - - {% endif %} + + External ID + {{ team.externalid }} + ICPC ID @@ -126,10 +124,7 @@ Affiliation - {% set affiliationId = team.affiliation.affilid %} - {% if showExternalId(team.affiliation) %} - {% set affiliationId = team.affiliation.externalid %} - {% endif %} + {% set affiliationId = team.affiliation.externalid %} {% set affiliationLogo = affiliationId | assetPath('affilation') %} {% if affiliationLogo %} {{ team.affiliation.shortname }} - {% set teamId = team.teamid %} - {% if showExternalId(team) %} - {% set teamId = team.externalid %} - {% endif %} + {% set teamId = team.externalid %} {% set teamImage = teamId | assetPath('team') %} {% if teamImage %}
- Picture of team {{ team.name }}
{% endif %} diff --git a/webapp/templates/jury/team_affiliation.html.twig b/webapp/templates/jury/team_affiliation.html.twig index 3609b3d3c53..1de78a3446c 100644 --- a/webapp/templates/jury/team_affiliation.html.twig +++ b/webapp/templates/jury/team_affiliation.html.twig @@ -19,12 +19,10 @@ ID {{ teamAffiliation.affilid }} - {% if showExternalId(teamAffiliation) %} - - External ID - {{ teamAffiliation.externalid }} - - {% endif %} + + External ID + {{ teamAffiliation.externalid }} + ICPC ID @@ -42,10 +40,7 @@ Logo - {% set affiliationId = teamAffiliation.affilid %} - {% if showExternalId(teamAffiliation) %} - {% set affiliationId = teamAffiliation.externalid %} - {% endif %} + {% set affiliationId = teamAffiliation.externalid %} {% set affiliationLogo = affiliationId | assetPath('affiliation') %} {% if affiliationLogo %} {{ teamAffiliation.shortname }}ID {{ teamCategory.categoryid }} - {% if showExternalId(teamCategory) %} - - External ID - {{ teamCategory.externalid }} - - {% endif %} + + External ID + {{ teamCategory.externalid }} + ICPC ID diff --git a/webapp/templates/jury/user.html.twig b/webapp/templates/jury/user.html.twig index f75f67701b6..2982b85c76c 100644 --- a/webapp/templates/jury/user.html.twig +++ b/webapp/templates/jury/user.html.twig @@ -19,12 +19,10 @@ ID {{ user.userid }} - {% if showExternalId(user) %} - - External ID - {{ user.externalid }} - - {% endif %} + + External ID + {{ user.externalid }} + Login {{ user.username }} diff --git a/webapp/templates/jury/versions.html.twig b/webapp/templates/jury/versions.html.twig index 0024aa6290f..e0171db6274 100644 --- a/webapp/templates/jury/versions.html.twig +++ b/webapp/templates/jury/versions.html.twig @@ -15,7 +15,14 @@ {% for lang in data %}
-
Language {{ lang.language.langid }}
+
+ Language {{ lang.language.langid }} + {% if is_granted('ROLE_ADMIN') %} + + {{ button(path('jury_language_edit', {'langId': lang.language.langid}), 'Edit version command(s)', 'primary btn-sm', 'edit') }} + + {% endif %} +
diff --git a/webapp/templates/partials/problem_list.html.twig b/webapp/templates/partials/problem_list.html.twig index 11a8712c337..bda100c09de 100644 --- a/webapp/templates/partials/problem_list.html.twig +++ b/webapp/templates/partials/problem_list.html.twig @@ -1,6 +1,22 @@ {# problem \App\Entity\ContestProblem #} -

{{ contest.name | default('Contest') }} problems

+{% set contest_problemset_add_cid = contest_problemset_add_cid | default(false) %} + +

+ {{ contest.name | default('Contest') }} problems + {% if contest and show_contest_problemset and contest.contestProblemsetType is not empty %} + {% if contest_problemset_add_cid %} + {% set contest_problemset_url = path(contest_problemset_path, {'cid': contest.cid}) %} + {% else %} + {% set contest_problemset_url = path(contest_problemset_path) %} + {% endif %} + + + problemset + + {% endif %} +

{% if problems is empty %}
No problem texts available at this point.
@@ -76,8 +92,8 @@
{% endfor %} @@ -90,11 +106,11 @@ {% endif %}
- {% if problem.problem.problemtextType is not empty %} + {% if problem.problem.problemstatementType is not empty %} - - text + href="{{ path(problem_statement_path, {'probId': problem.probid}) }}"> + + statement {% endif %} diff --git a/webapp/templates/partials/scoreboard.html.twig b/webapp/templates/partials/scoreboard.html.twig index 48c211ed4cc..3c4f58e4e02 100644 --- a/webapp/templates/partials/scoreboard.html.twig +++ b/webapp/templates/partials/scoreboard.html.twig @@ -20,7 +20,7 @@
{{ current_contest.name }} - + {% if scoreboard is null %} {{ current_contest | printContestStart }} {% elseif scoreboard.freezeData.showFinal(jury) %} @@ -93,11 +93,12 @@
{% endif %} - {% if scoreboard.freezeData.showFrozen(false) %} + {% if scoreboard.freezeData.showFrozen %}