diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php index 7c34ed1..5f74152 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php @@ -19,7 +19,8 @@ * description = @Translation("Create and store date ranges."), * default_widget = "daterange_default", * default_formatter = "daterange_default", - * list_class = "\Drupal\datetime_range\Plugin\Field\FieldType\DateRangeFieldItemList" + * list_class = "\Drupal\datetime_range\Plugin\Field\FieldType\DateRangeFieldItemList", + * constraints = {"DateRangeFormat" = {}, "DateRangeStartEnd" = {}} * ) */ class DateRangeItem extends DateTimeItem { diff --git a/core/modules/datetime_range/src/Plugin/Validation/Constraint/DateRangeFormatConstraint.php b/core/modules/datetime_range/src/Plugin/Validation/Constraint/DateRangeFormatConstraint.php new file mode 100644 index 0000000..ecfe31c --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Validation/Constraint/DateRangeFormatConstraint.php @@ -0,0 +1,59 @@ +getValue()['value']; + $end_value = $item->getValue()['end_value']; + + if (!is_string($value)) { + $this->context->addViolation($constraint->badStartType); + $stop = TRUE; + } + if (!is_string($end_value)) { + $this->context->addViolation($constraint->badEndType); + $stop = TRUE; + } + + if ($stop) return; + + $datetime_type = $item->getFieldDefinition()->getSetting('datetime_type'); + $format = $datetime_type === DateRangeItem::DATETIME_TYPE_DATE ? DateTimeItemInterface::DATE_STORAGE_FORMAT : DateTimeItemInterface::DATETIME_STORAGE_FORMAT; + + $start_date = NULL; + try { + $start_date = DateTimePlus::createFromFormat($format, $value, new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE)); + } + catch (\InvalidArgumentException $e) { + $this->context->addViolation($constraint->badStartFormat, [ + '@value' => $value, + '@format' => $format, + ]); + $stop = TRUE; + } + catch (\UnexpectedValueException $e) { + $this->context->addViolation($constraint->badStartValue, [ + '@value' => $value, + '@format' => $format, + ]); + $stop = TRUE; + } + + $end_date = NULL; + try { + $end_date = DateTimePlus::createFromFormat($format, $end_value, new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE)); + } + catch (\InvalidArgumentException $e) { + $this->context->addViolation($constraint->badEndFormat, [ + '@end_value' => $end_value, + '@format' => $format, + ]); + $stop = TRUE; + } + catch (\UnexpectedValueException $e) { + $this->context->addViolation($constraint->badEndValue, [ + '@end_value' => $end_value, + '@format' => $format, + ]); + $stop = TRUE; + } + + if ($stop) return; + + if ($start_date === NULL || $start_date->hasErrors()) { + $this->context->addViolation($constraint->badStartFormat, [ + '@value' => $value, + '@format' => $format, + ]); + } + + if ($end_date === NULL || $end_date->hasErrors()) { + $this->context->addViolation($constraint->badEndFormat, [ + '@end_value' => $end_value, + '@format' => $format, + ]); + } + } + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Validation/Constraint/DateRangeStartEndConstraint.php b/core/modules/datetime_range/src/Plugin/Validation/Constraint/DateRangeStartEndConstraint.php new file mode 100644 index 0000000..74e8fa3 --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Validation/Constraint/DateRangeStartEndConstraint.php @@ -0,0 +1,24 @@ +getValue()['value']; + $end_value = $item->getValue()['end_value']; + + // We cannot get the computed values from the item at this point, so we + // have to use string comparison. This may give false error messages, but + // the format constraints will also fail and provide a proper message for + // the user. + if (is_string($value) && is_string($end_value)) { + if ($value > $end_value) { + $this->context->addViolation($constraint->badStartEnd, [ + '@value' => $value, + '@end_value' => $end_value, + ]); + } + } + } + } + +} diff --git a/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestAlldayTest.php b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestAlldayTest.php new file mode 100644 index 0000000..f74bc4b --- /dev/null +++ b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestAlldayTest.php @@ -0,0 +1,101 @@ +entity->getEntityType()->hasKey('bundle')) { + $fieldName = static::$fieldName; + $fieldFormat = DateTimeItemInterface::DATETIME_STORAGE_FORMAT; + + // DX: 422 when date type is incorrect. + + $value = ['2017', '03', '01', '21', '53', '00']; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value must be a string.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = ['2017', '03', '02', '21', '53', '00']; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value must be a string.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = ['2017', '03', '01', '21', '53', '00']; + $end_value = ['2017', '03', '02', '21', '53', '00']; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value must be a string.\n{$fieldName}.0: The date range end value must be a string.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when date format is incorrect. + + $value = '2017-03-01'; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = '2017-03-02'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = '2017-03-01'; + $end_value = '2017-03-02'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' is invalid for the format '{$fieldFormat}'\n{$fieldName}.0: The date range end value '{$end_value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when date format is incorrect. + + $value = '2016-13-55T20:02:00'; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = '2018-13-56T20:02:00'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = '2016-13-55T20:02:00'; + $end_value = '2018-13-56T20:02:00'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0: The date range end value '{$end_value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when the end date is before the start date. + + $value = static::$endDateString; + $end_value = static::$startDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' must be equal to or after the start value '{$value}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + } + } + +} diff --git a/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateonlyTest.php b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateonlyTest.php new file mode 100644 index 0000000..72f1e3b --- /dev/null +++ b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateonlyTest.php @@ -0,0 +1,101 @@ +entity->getEntityType()->hasKey('bundle')) { + $fieldName = static::$fieldName; + $fieldFormat = DateTimeItemInterface::DATE_STORAGE_FORMAT; + + // DX: 422 when date type is incorrect. + + $value = ['2017', '03', '01']; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value must be a string.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = ['2017', '03', '02']; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value must be a string.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = ['2017', '03', '01']; + $end_value = ['2017', '03', '02']; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value must be a string.\n{$fieldName}.0: The date range end value must be a string.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when date format is incorrect. + + $value = '2017-03-01T20:02:00'; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = '2017-03-02T20:02:00'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = '2017-03-01T20:02:00'; + $end_value = '2017-03-02T20:02:00'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' is invalid for the format '{$fieldFormat}'\n{$fieldName}.0: The date range end value '{$end_value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when date format is incorrect. + + $value = '2016-13-55'; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = '2018-13-56'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = '2016-13-55'; + $end_value = '2018-13-56'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0: The date range end value '{$end_value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when the end date is before the start date. + + $value = static::$endDateString; + $end_value = static::$startDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' must be equal to or after the start value '{$value}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + } + } + +} diff --git a/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDaterangeTest.php b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDaterangeTest.php new file mode 100644 index 0000000..83919ef --- /dev/null +++ b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDaterangeTest.php @@ -0,0 +1,101 @@ +entity->getEntityType()->hasKey('bundle')) { + $fieldName = static::$fieldName; + $fieldFormat = DateTimeItemInterface::DATETIME_STORAGE_FORMAT; + + // DX: 422 when date type is incorrect. + + $value = ['2017', '03', '01', '21', '53', '00']; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value must be a string.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = ['2017', '03', '02', '21', '53', '00']; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value must be a string.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = ['2017', '03', '01', '21', '53', '00']; + $end_value = ['2017', '03', '02', '21', '53', '00']; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value must be a string.\n{$fieldName}.0: The date range end value must be a string.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when date format is incorrect. + + $value = '2017-03-01'; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = '2017-03-02'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = '2017-03-01'; + $end_value = '2017-03-02'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' is invalid for the format '{$fieldFormat}'\n{$fieldName}.0: The date range end value '{$end_value}' is invalid for the format '{$fieldFormat}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when date format is incorrect. + + $value = '2016-13-55T20:02:00'; + $end_value = static::$endDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = static::$startDateString; + $end_value = '2018-13-56T20:02:00'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + $value = '2016-13-55T20:02:00'; + $end_value = '2018-13-56T20:02:00'; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range start value '{$value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0: The date range end value '{$end_value}' did not parse properly for the format '{$fieldFormat}'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + + // DX: 422 when the end date is before the start date. + + $value = static::$endDateString; + $end_value = static::$startDateString; + $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The date range end value '{$end_value}' must be equal to or after the start value '{$value}'\n"; + $this->doEdgeCaseCall($method, $url, $request_options, $value, $end_value, $message); + } + } + +} diff --git a/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDaterangeTestBase.php b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDaterangeTestBase.php new file mode 100644 index 0000000..d989ec4 --- /dev/null +++ b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDaterangeTestBase.php @@ -0,0 +1,163 @@ + static::$fieldName, + 'type' => 'daterange', + 'entity_type' => static::$entityTypeId, + 'settings' => ['datetime_type' => static::$datetimeType], + ]) + ->save(); + + FieldConfig::create([ + 'field_name' => static::$fieldName, + 'entity_type' => static::$entityTypeId, + 'bundle' => $this->entity->bundle(), + ]) + ->save(); + + // Reload entity so that it has the new field. + $this->entity = $this->entityStorage->load($this->entity->id()); + $this->entity->set(static::$fieldName, [ + 'value' => static::$startDateString, + 'end_value' => static::$endDateString, + ]); + $this->entity->save(); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $entity_test = EntityTest::create([ + 'name' => 'Llama', + 'type' => static::$entityTypeId, + static::$fieldName => [ + 'value' => static::$startDateString, + 'end_value' => static::$endDateString, + ], + ]); + $entity_test->setOwnerId(0); + $entity_test->save(); + + return $entity_test; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return parent::getExpectedNormalizedEntity() + [ + static::$fieldName => [ + [ + 'value' => $this->entity->get(static::$fieldName)->value, + 'end_value' => $this->entity->get(static::$fieldName)->end_value, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + static::$fieldName => [ + [ + 'value' => static::$startDateString, + 'end_value' => static::$endDateString, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) { + parent::assertNormalizationEdgeCases($method, $url, $request_options); + } + + /** + * Performs a REST call to test an edge case. + * + * @param string $method + * HTTP method. + * @param \Drupal\Core\Url $url + * URL to request. + * @param array $request_options + * Request options to apply. + * @var mixed $value + * The test start date value. + * @var mixed $end_value + * The test end date value. + * @var string $message + * The expected error message. + */ + protected function doEdgeCaseCall($method, Url $url, array $request_options, $value, $end_value, $message) { + $normalization = $this->getNormalizedPostEntity(); + $normalization[static::$fieldName][0]['value'] = $value; + $normalization[static::$fieldName][0]['end_value'] = $end_value; + + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + $response = $this->request($method, $url, $request_options); + $this->assertResourceErrorResponse(422, $message, $response); + } + +}