diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index 4716857940..b8c4c5e419 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -956,9 +956,24 @@ private function checkTimezones(FormInterface $form): ?Response foreach (['Activate', 'Deactivate', 'Start', 'End', 'Freeze', 'Unfreeze'] as $timeString) { $tmpValue = $formData->{'get' . $timeString . 'timeString'}(); if ($tmpValue !== '' && !is_null($tmpValue)) { - $fields = explode(' ', $tmpValue); - if (count($fields) > 1) { - $timeZones[] = $fields[2]; + if (preg_match("/\d{2}-\d{2}-\d{2}.*/", $tmpValue) === 1) { + $chr = $tmpValue[10]; // The separator between date & time + $fields = explode($chr, $tmpValue); + // First field is the time, 2nd/3th might be timezone or offset + $tmpValue = substr(str_replace($fields[0], '', $tmpValue), 1); // Also remove the separator + if (str_contains($tmpValue, ' ')) { + $fields = explode(' ', $tmpValue); + } elseif (str_contains($tmpValue, '+')) { + $fields = explode('+', $tmpValue); + } elseif (str_contains($tmpValue, '-')) { + $fields = explode('-', $tmpValue); + } elseif (substr($tmpValue, -1) === 'Z') { + $timeZones[] = 'UTC'; + continue; + } + if (count($fields) > 1) { + $timeZones[] = $fields[1]; + } } } } diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index 03a7207594..0b8b4766fe 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -1275,7 +1275,7 @@ public function validate(ExecutionContextInterface $context): void $this->updateTimes(); if (Utils::difftime((float)$this->getEndtime(), (float)$this->getStarttime(true)) <= 0) { $context - ->buildViolation('Contest ends before it even starts') + ->buildViolation('Contest ends before it even starts.') ->atPath('endtimeString') ->addViolation(); } diff --git a/webapp/src/Validator/Constraints/TimeStringValidator.php b/webapp/src/Validator/Constraints/TimeStringValidator.php index 9a47c47b4d..f7ca0bff20 100644 --- a/webapp/src/Validator/Constraints/TimeStringValidator.php +++ b/webapp/src/Validator/Constraints/TimeStringValidator.php @@ -10,6 +10,10 @@ class TimeStringValidator extends ConstraintValidator { public function validate(mixed $value, Constraint $constraint): void { + $timezoneRegex = "[A-Za-z][A-Za-z0-9_\/+-]{1,35}"; # See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + $offsetRegex = "[+-]\d{1,2}(:\d\d)?"; + $absoluteRegex = "\d\d\d\d-\d\d-\d\d( |T)\d\d:\d\d:\d\d(\.\d{1,6})?( " . $timezoneRegex . "|" . $offsetRegex . "|Z)"; + $relativeRegex = "\d+:\d\d(:\d\d(\.\d{1,6})?)?"; if (!$constraint instanceof TimeString) { throw new UnexpectedTypeException($constraint, TimeString::class); } @@ -24,11 +28,11 @@ public function validate(mixed $value, Constraint $constraint): void if ($constraint->allowRelative) { $regex = $constraint->relativeIsPositive ? - "/^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d(\.\d{1,6})? [A-Za-z][A-Za-z0-9_\/+-]{1,35}|\+\d+:\d\d(:\d\d(\.\d{1,6})?)?)$/" : - "/^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d(\.\d{1,6})? [A-Za-z][A-Za-z0-9_\/+-]{1,35}|-\d+:\d\d(:\d\d(\.\d{1,6})?)?)$/"; + "/^(" . $absoluteRegex . "|\+?" . $relativeRegex . ")$/" : + "/^(" . $absoluteRegex . "|-" . $relativeRegex . ")$/"; $message = $constraint->absoluteRelativeMessage; } else { - $regex = "/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d(\.\d{1,6})? [A-Za-z][A-Za-z0-9_\/+-]{1,35}$/"; + $regex = "/^" . $absoluteRegex . "$/"; $message = $constraint->absoluteMessage; } if (preg_match($regex, $value) !== 1) { diff --git a/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php b/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php index a3b0fba1cf..b3ed5e127d 100644 --- a/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php @@ -49,6 +49,54 @@ class ContestControllerTest extends JuryControllerTestCase 'silverMedals' => '1', 'bronzeMedals' => '1', 'medalCategories' => ['0' => '2']], + ['shortname' => 'CLICS_offset_HMM', + 'name' => 'No Timezone but only offset', + 'activatetimeString' => '2021-07-17 16:08:00+1:11', + 'starttimeString' => '2021-07-17 16:09:00+1:11', + 'freezetimeString' => '2021-07-17 16:10:00+1:11', + 'endtimeString' => '2021-07-17 16:11:00+1:11', + 'unfreezetimeString' => '2021-07-17T16:12:00+1:11', + 'deactivatetimeString' => '2021-07-17T16:13:00+1:11'], + ['shortname' => 'CLICS_offset_HHMM', + 'name' => 'No Timezone but only offset', + 'activatetimeString' => '2021-07-17 16:08:00+11:11', + 'starttimeString' => '2021-07-17 16:09:00+11:11', + 'freezetimeString' => '2021-07-17 16:10:00+11:11', + 'endtimeString' => '2021-07-17 16:11:00+11:11', + 'unfreezetimeString' => '2021-07-17T16:12:00+11:11', + 'deactivatetimeString' => '2021-07-17T16:13:00+11:11'], + ['shortname' => 'CLICS_offset_0H00', + 'name' => 'No Timezone but only offset', + 'activatetimeString' => '2021-07-17 16:08:00+01:00', + 'starttimeString' => '2021-07-17 16:09:00+01:00', + 'freezetimeString' => '2021-07-17 16:10:00+01:00', + 'endtimeString' => '2021-07-17 16:11:00+01:00', + 'unfreezetimeString' => '2021-07-17T16:12:00+01:00', + 'deactivatetimeString' => '2021-07-17T16:13:00+01:00'], + ['shortname' => 'CLICS_offset_H', + 'name' => 'No Timezone but only offset', + 'activatetimeString' => '2021-07-17 16:08:00+1', + 'starttimeString' => '2021-07-17 16:09:00+1', + 'freezetimeString' => '2021-07-17 16:10:00+1', + 'endtimeString' => '2021-07-17 16:11:00+1', + 'unfreezetimeString' => '2021-07-17T16:12:00+1', + 'deactivatetimeString' => '2021-07-17T16:13:00+1'], + ['shortname' => 'CLICS_offset_-HHH', + 'name' => 'No Timezone but only offset', + 'activatetimeString' => '2021-07-17 16:08:00-01', + 'starttimeString' => '2021-07-17 16:09:00-01', + 'freezetimeString' => '2021-07-17 16:10:00-01', + 'endtimeString' => '2021-07-17 16:11:00-01', + 'unfreezetimeString' => '2021-07-17T16:12:00-01', + 'deactivatetimeString' => '2021-07-17T16:13:00-01'], + ['shortname' => 'utc_Z', + 'name' => 'UTC (Z)', + 'activatetimeString' => '2021-07-17 16:08:00Z', + 'starttimeString' => '2021-07-17 16:09:00.0Z', + 'freezetimeString' => '2021-07-17 16:10:00.00Z', + 'endtimeString' => '2021-07-17 16:11:00.000Z', + 'unfreezetimeString' => '2021-07-17T16:12:00.1Z', + 'deactivatetimeString' => '2021-07-17T16:13:00.2Z'], ['shortname' => 'otzcet', 'name' => 'Other timezone (CET)', 'activatetimeString' => '2021-07-17 16:08:00 CET', @@ -57,6 +105,14 @@ class ContestControllerTest extends JuryControllerTestCase 'endtimeString' => '2021-07-17 16:11:00 CET', 'unfreezetimeString' => '2021-07-17 16:12:00 CET', 'deactivatetimeString' => '2021-07-17 16:13:00 CET'], + ['shortname' => 'otzAfricaPorto-Novo', + 'name' => 'Other timezone (Africa/Porto-Novo)', + 'activatetimeString' => '2021-07-17 16:08:00 Africa/Porto-Novo', + 'starttimeString' => '2021-07-17 16:09:00 Africa/Porto-Novo', + 'freezetimeString' => '2021-07-17 16:10:00 Africa/Porto-Novo', + 'endtimeString' => '2021-07-17 16:11:00 Africa/Porto-Novo', + 'unfreezetimeString' => '2021-07-17 16:12:00 Africa/Porto-Novo', + 'deactivatetimeString' => '2021-07-17 16:13:00 Africa/Porto-Novo'], ['shortname' => 'otzunder', 'name' => 'Other timezone (Underscore)', 'activatetimeString' => '2021-07-17 16:08:00 America/Porto_Velho', @@ -73,6 +129,14 @@ class ContestControllerTest extends JuryControllerTestCase 'endtimeString' => '2021-07-17 16:11:00 Etc/GMT-3', 'unfreezetimeString' => '', 'deactivatetimeString' => ''], + ['shortname' => 'otzGMT2', + 'name' => 'Other timezone (GMT)', + 'activatetimeString' => '2021-07-17 16:08:00 Etc/GMT+3', + 'starttimeString' => '2021-07-17 16:09:00 Etc/GMT+3', + 'freezetimeString' => '2021-07-17 16:10:00 Etc/GMT+3', + 'endtimeString' => '2021-07-17 16:11:00 Etc/GMT+3', + 'unfreezetimeString' => '', + 'deactivatetimeString' => ''], ['shortname' => 'otzrel', 'name' => 'Other timezone (Relative)', 'activatetimeString' => '-10:00', @@ -81,6 +145,14 @@ class ContestControllerTest extends JuryControllerTestCase 'endtimeString' => '+1111:11', 'unfreezetimeString' => '', 'deactivatetimeString' => ''], + ['shortname' => 'other_split_char', + 'name' => 'Other CLICS splitchar', + 'activatetimeString' => '-10:00', + 'starttimeString' => '2021-07-17T16:09:00 Atlantic/Reykjavik', + 'freezetimeString' => '+0:01', + 'endtimeString' => '+1111:11', + 'unfreezetimeString' => '', + 'deactivatetimeString' => ''], ['shortname' => 'nofr', 'name' => 'No Freeze', 'freezetimeString' => '', @@ -93,7 +165,7 @@ class ContestControllerTest extends JuryControllerTestCase 'endtimeString' => '2021-07-17 16:11:00 Europe/Amsterdam', 'unfreezetimeString' => '', 'deactivatetimeString' => ''], - ['shortname' => 'dirfreeze', + ['shortname' => 'dirfreeze', 'name' => 'Direct freeze minimal', 'activatetimeString' => '2021-07-17 16:07:59 Europe/Amsterdam', 'starttimeString' => '2021-07-17 16:08:00 Europe/Amsterdam', @@ -109,6 +181,14 @@ class ContestControllerTest extends JuryControllerTestCase 'endtimeString' => '+10:00', 'unfreezetimeString' => '+25:00', 'deactivatetimeString' => ''], + ['shortname' => 'dirfreezerelnoplus', + 'name' => 'Direct freeze minimal relative', + 'activatetimeString' => '-0:00', + 'starttimeString' => '2021-07-17 16:08:00 Europe/Amsterdam', + 'freezetimeString' => '0:00', + 'endtimeString' => '+10:00', + 'unfreezetimeString' => '25:00', + 'deactivatetimeString' => ''], ['shortname' => 'rel', 'name' => 'Relative contest', 'activatetimeString' => '-1:00',