diff --git a/pfSense-pkg-API/files/etc/inc/api/core/Field.inc b/pfSense-pkg-API/files/etc/inc/api/core/Field.inc index 4041f7b26..ad1f65020 100644 --- a/pfSense-pkg-API/files/etc/inc/api/core/Field.inc +++ b/pfSense-pkg-API/files/etc/inc/api/core/Field.inc @@ -323,7 +323,7 @@ class Field $this->value = null; return true; } - + # Ensure this field object has a name set $this->check_field_names(); @@ -642,7 +642,7 @@ class Field * @param mixed $value The value to check against assigned `type`. * @throws ValidationError If the specified value is not a supported type. */ - private function check_value_type(mixed $value) { + protected function check_value_type(mixed $value) { # Don't validate type for `read_only` fields if ($this->read_only) { return; diff --git a/pfSense-pkg-API/files/etc/inc/api/fields/DateTimeField.inc b/pfSense-pkg-API/files/etc/inc/api/fields/DateTimeField.inc new file mode 100644 index 000000000..dddcc78f9 --- /dev/null +++ b/pfSense-pkg-API/files/etc/inc/api/fields/DateTimeField.inc @@ -0,0 +1,160 @@ + "type1"] to this parameter. + * @param array $validators An array of Validator objects to run against this field. + * @param string $help_text Set a description for this field. This description will be used in API documentation. + */ + public function __construct( + bool $required = false, + bool $unique = false, + mixed $default = null, + array $choices = [], + string $choices_callable = "", + string $datetime_format = "m/d/Y", + bool $allow_empty = false, + bool $allow_null = false, + bool $editable = true, + bool $read_only = false, + bool $write_only = false, + bool $representation_only = false, + bool $many = false, + int $many_minimum = 1, + int $many_maximum = 128, + string|null $delimiter = ",", + string $verbose_name = "", + string $verbose_name_plural = "", + string $internal_name = "", + string $internal_namespace = "", + array $referenced_by = [], + array $conditions = [], + array $validators = [], + string $help_text = "" + ) + { + $this->datetime_format = $datetime_format; + parent::__construct( + type: "string", + required: $required, + unique: $unique, + default: $default, + choices: $choices, + choices_callable: $choices_callable, + allow_empty: $allow_empty, + allow_null: $allow_null, + editable: $editable, + read_only: $read_only, + write_only: $write_only, + representation_only: $representation_only, + many: $many, + many_minimum: $many_minimum, + many_maximum: $many_maximum, + delimiter: $delimiter, + verbose_name: $verbose_name, + verbose_name_plural: $verbose_name_plural, + internal_name: $internal_name, + internal_namespace: $internal_namespace, + referenced_by: $referenced_by, + conditions: $conditions, + validators: $validators, + help_text: $help_text + ); + } + + /** + * @param string $datetime The datetime string to validate. (i.e. 12/31/1999) + * @param string $format The DateTime format of the $datetime string. (i.e. m/d/Y) + * @return bool `true` when the datetime string is valid, `false` when it is not. + */ + public static function is_valid_datetime(string $datetime, string $format) : bool { + # Try to create a DateTime object with this datetime string and format, then check for errors + $datetime_obj = DateTime::createFromFormat($format, $datetime); + $errors = DateTime::getLastErrors(); + + # This is not a valid datetime string if there are warnings + if ($errors["warning_count"]) { + return false; + } + + return $datetime_obj !== false; + } + + protected function check_value_type(mixed $value) + { + # Run the parent check_value_type() method to check primitive date type (i.e. string, boolean, integer) + parent::check_value_type($value); + + # Also check to ensure that the datetime value matches this Field's DateTime format. + if (!$this->is_valid_datetime($value, $this->datetime_format)) { + throw new ValidationError( + message: "Field `$this->name` must be a DateTime in format `$this->datetime_format`. Received `$value`", + response_id: "DATETIME_FIELD_MUST_MATCH_FORMAT" + ); + } + } +} diff --git a/pfSense-pkg-API/files/etc/inc/api/tests/APIFieldsDateTimeFieldTestCase.inc b/pfSense-pkg-API/files/etc/inc/api/tests/APIFieldsDateTimeFieldTestCase.inc new file mode 100644 index 000000000..f5d199977 --- /dev/null +++ b/pfSense-pkg-API/files/etc/inc/api/tests/APIFieldsDateTimeFieldTestCase.inc @@ -0,0 +1,49 @@ +assert_is_true(DateTimeField::is_valid_datetime("12/31/1999", "m/d/Y")); + $this->assert_is_true(DateTimeField::is_valid_datetime("Dec 31, 1999", "M d, Y")); + + # Ensure invalid datetimes return false + $this->assert_is_false(DateTimeField::is_valid_datetime("13/31/1999", "m/d/Y")); + $this->assert_is_false(DateTimeField::is_valid_datetime("Test 31, 1999", "M d, Y")); + $this->assert_is_false(DateTimeField::is_valid_datetime("Dec 31, 1999", "m/d/Y")); + $this->assert_is_false(DateTimeField::is_valid_datetime("12/31/1999", "M d, Y")); + } + + /** + * Checks that an error is thrown if this field is validated and its value does not match is datetime format + */ + public function test_validate() { + $this->assert_throws_response( + response_id: "DATETIME_FIELD_MUST_MATCH_FORMAT", + code: 400, + callable: function () { + $dt = new DateTimeField(required: true, datetime_format: "m/d/Y"); + $dt->name = "test_field"; + $dt->value = "not a datetime"; + $dt->validate(); + } + ); + + $this->assert_does_not_throw( + callable: function () { + $dt = new DateTimeField(required: true, datetime_format: "m/d/Y"); + $dt->name = "test_field"; + $dt->value = "12/31/1999"; + $dt->validate(); + } + ); + } +}