Skip to content

Commit 0308b84

Browse files
authored
Merge pull request #86 from simplesamlphp/feature/strip-trailing-zeros
Be lenient in what we accept, like dateTime/time with trailing sub-se…
2 parents d3207b4 + bb3f693 commit 0308b84

10 files changed

Lines changed: 140 additions & 27 deletions

File tree

.github/workflows/php.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ jobs:
144144
- uses: actions/checkout@v6
145145

146146
- name: Get composer cache directory
147-
run: echo COMPOSER_CACHE="$(composer config cache-files-dir)" >> "$env:GITHUB_ENV"
147+
run: echo COMPOSER_CACHE="$(composer config cache-files-dir)" >> "$GITHUB_ENV"
148148

149149
- name: Get COMPOSER_ROOT_VERSION from composer.json branch alias
150150
shell: bash
@@ -154,7 +154,7 @@ jobs:
154154
echo "Could not read extra.branch-alias.dev-master from composer.json" >&2
155155
exit 1
156156
fi
157-
echo "COMPOSER_ROOT_VERSION=$ROOT_VERSION" >> "GITHUB_ENV"
157+
echo COMPOSER_ROOT_VERSION="$ROOT_VERSION" >> "$GITHUB_ENV"
158158
159159
- name: Cache composer dependencies
160160
uses: actions/cache@v5

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"simplesamlphp/composer-xmlprovider-installer": "~1.3"
5050
},
5151
"require-dev": {
52-
"simplesamlphp/simplesamlphp-test-framework": "~1.11"
52+
"simplesamlphp/simplesamlphp-test-framework": "~1.11.5"
5353
},
5454
"support": {
5555
"issues": "https://github.com/simplesamlphp/xml-common/issues",

src/XML/Assert/DateTimeTrait.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,6 @@ trait DateTimeTrait
3131
* * '.' s+ (if present) represents the fractional seconds;
3232
* * zzzzzz (if present) represents the timezone (as described below).
3333
*
34-
* Except for trailing fractional zero digits in the seconds representation, '24:00:00' time representations,
35-
* and timezone (for timezoned values), the mapping from literals to values is one-to-one.
36-
* Where there is more than one possible representation, the canonical representation is as follows:
37-
38-
* The 2-digit numeral representing the hour must not be '24';
39-
* The fractional second string, if present, must not end in '0';
40-
* for timezoned values, the timezone must be represented with 'Z' (All timezoned dateTime values are UTC.).
41-
*
42-
*
4334
* Note: we're restricting decimal seconds to 12, although strictly the standards allow an infite number.
4435
*
4536
* We know for a fact that Apereo CAS v7.0.x uses 9 decimals
@@ -65,7 +56,7 @@ trait DateTimeTrait
6556
([0-1][0-9]|2[0-3])
6657
:(0[0-9]|[1-5][0-9])
6758
:(0[0-9]|[1-5][0-9])
68-
(\.[0-9]{0,11}[1-9])?
59+
(\.[0-9]{0,12})?
6960
(
7061
[+-]([0-1][0-9]|2[0-4])
7162
:(0[0-9]|[1-5][0-9])

src/XML/Assert/TimeTrait.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,6 @@ trait TimeTrait
2424
* * '.' s+ (if present) represents the fractional seconds;
2525
* * zzzzzz (if present) represents the timezone (as described below).
2626
*
27-
* Except for trailing fractional zero digits in the seconds representation, '24:00:00' time representations,
28-
* and timezone (for timezoned values), the mapping from literals to values is one-to-one.
29-
* Where there is more than one possible representation, the canonical representation is as follows:
30-
*
31-
* The 2-digit numeral representing the hour must not be '24';
32-
* The fractional second string, if present, must not end in '0';
33-
* for timezoned values, the timezone must be represented with 'Z' (All timezoned dateTime values are UTC.).
34-
*
3527
* Note: we're restricting decimal seconds to 12, although strictly the standards allow an infite number.
3628
*
3729
* We know for a fact that Apereo CAS v7.0.x uses 9 decimals
@@ -41,7 +33,7 @@ trait TimeTrait
4133
:
4234
(0[0-9]|[1-5][0-9])
4335
:(0[0-9]|[1-5][0-9])
44-
(\.[0-9]{0,11}[1-9])?
36+
(\.[0-9]{0,12})?
4537
([+-]([0-1][0-9]|2[0-4]):(0[0-9]|[1-5][0-9])|Z)?
4638
$/Dx';
4739

src/XMLSchema/Type/DateTimeValue.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111
use SimpleSAML\XMLSchema\Exception\SchemaViolationException;
1212
use SimpleSAML\XMLSchema\Type\Interface\AbstractAnySimpleType;
1313

14+
use function preg_replace;
15+
1416
/**
1517
* @package simplesaml/xml-common
1618
*/
1719
class DateTimeValue extends AbstractAnySimpleType
1820
{
1921
public const string SCHEMA_TYPE = 'dateTime';
2022

21-
public const string DATETIME_FORMAT = 'Y-m-d\\TH:i:sP';
23+
public const string DATETIME_FORMAT = 'Y-m-d\\TH:i:s.uP';
2224

2325

2426
/**
@@ -28,7 +30,12 @@ class DateTimeValue extends AbstractAnySimpleType
2830
*/
2931
protected function sanitizeValue(string $value): string
3032
{
31-
return static::collapseWhitespace(static::normalizeWhitespace($value));
33+
$normalized = static::collapseWhitespace(static::normalizeWhitespace($value));
34+
$sanitized = preg_replace('/\.(\d{0,6})\d*/', '.$1', $normalized);
35+
36+
// Remove all trailing zeros after the dot, and remove the dot if only zeros were present
37+
$sanitized = preg_replace('/\.(?=\d)(?:\d*?[1-9])?\K0+(?=[^0-9]|$)/', '', $sanitized);
38+
return preg_replace('/\.(?!\d)/', '', $sanitized);
3239
}
3340

3441

src/XMLSchema/Type/TimeValue.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44

55
namespace SimpleSAML\XMLSchema\Type;
66

7+
use DateTimeImmutable;
8+
use DateTimeInterface;
9+
use Psr\Clock\ClockInterface;
710
use SimpleSAML\XML\Assert\Assert;
811
use SimpleSAML\XMLSchema\Exception\SchemaViolationException;
912
use SimpleSAML\XMLSchema\Type\Interface\AbstractAnySimpleType;
1013

14+
use function preg_replace;
15+
1116
/**
1217
* @package simplesaml/xml-common
1318
*/
1419
class TimeValue extends AbstractAnySimpleType
1520
{
1621
public const string SCHEMA_TYPE = 'time';
1722

23+
public const string DATETIME_FORMAT = 'H:i:s.uP';
24+
1825

1926
/**
2027
* Sanitize the value.
@@ -23,7 +30,12 @@ class TimeValue extends AbstractAnySimpleType
2330
*/
2431
protected function sanitizeValue(string $value): string
2532
{
26-
return static::collapseWhitespace(static::normalizeWhitespace($value));
33+
$normalized = static::collapseWhitespace(static::normalizeWhitespace($value));
34+
$sanitized = preg_replace('/\.(\d{0,6})\d*/', '.$1', $normalized);
35+
36+
// Remove all trailing zeros after the dot, and remove the dot if only zeros were present
37+
$sanitized = preg_replace('/\.(?=\d)(?:\d*?[1-9])?\K0+(?=[^0-9]|$)/', '', $sanitized);
38+
return preg_replace('/\.(?!\d)/', '', $sanitized);
2739
}
2840

2941

@@ -37,4 +49,29 @@ protected function validateValue(string $value): void
3749
{
3850
Assert::validTime($this->sanitizeValue($value), SchemaViolationException::class);
3951
}
52+
53+
54+
/**
55+
*/
56+
public static function now(ClockInterface $clock): static
57+
{
58+
return static::fromDateTime($clock->now());
59+
}
60+
61+
62+
/**
63+
* @param \DateTimeInterface $value
64+
*/
65+
public static function fromDateTime(DateTimeInterface $value): static
66+
{
67+
return new static($value->format(static::DATETIME_FORMAT));
68+
}
69+
70+
71+
/**
72+
*/
73+
public function toDateTime(): DateTimeImmutable
74+
{
75+
return new DateTimeImmutable($this->getValue());
76+
}
4077
}

tests/XML/Assert/DateTimeTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public static function provideValidDateTime(): array
4949
'valid with subseconds' => [true, '2001-10-26T21:32:52.12679'],
5050
'valid with more than four digit year' => [true, '-22001-10-26T21:32:52+02:00'],
5151
'valid with up to twelve sub-seconds' => [true, '2001-10-26T21:32:52.126798764382'],
52+
'sub-seconds ending with zero' => [true, '2001-10-26T21:32:52.12670'],
5253
];
5354
}
5455

@@ -67,7 +68,6 @@ public static function provideInvalidDateTime(): array
6768
'prefixed zero' => [false, '02001-10-26T25:32:52+02:00'],
6869
'wrong format' => [false, '01-10-26T21:32'],
6970
'too many sub-seconds' => [false, '2001-10-26T21:32:52.1267987643821'],
70-
'sub-seconds ending with zero' => [false, '2001-10-26T21:32:52.12670'],
7171
];
7272
}
7373
}

tests/XML/Assert/TimeTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public static function provideValidTime(): array
4747
'valid time with 00:00 timezone' => [true, '19:32:52+00:00'],
4848
'valid time with sub-seconds' => [true, '21:32:52.12679'],
4949
'valid with up to twelve sub-seconds' => [true, '21:32:52.126798764382'],
50+
'sub-seconds ending with zero' => [true, '21:32:52.12670'],
5051
];
5152
}
5253

@@ -62,7 +63,6 @@ public static function provideInvalidTime(): array
6263
'invalid hour twenty-four' => [false, '24:25:10'],
6364
'invalid invalid format' => [false, '1:20:10'],
6465
'too many sub-seconds' => [false, '21:32:52.1267987643821'],
65-
'sub-seconds ending with zero' => [false, '21:32:52.12670'],
6666
];
6767
}
6868
}

tests/XMLSchema/Type/DateTimeValueTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,49 @@ public function testFromDateTime(): void
5454
}
5555

5656

57+
/**
58+
*/
59+
public function testSubSeconds(): void
60+
{
61+
// Strip sub-second trailing zero's and make sure the decimal sign is removed
62+
$dateTimeValue = DateTimeValue::fromString('2001-10-26T21:32:52.00');
63+
$this->assertEquals('2001-10-26T21:32:52', $dateTimeValue->getValue());
64+
65+
// Strip sub-second trailing zero's
66+
$dateTimeValue = DateTimeValue::fromString('2001-10-26T21:32:52.12300');
67+
$this->assertEquals('2001-10-26T21:32:52.123', $dateTimeValue->getValue());
68+
69+
// Strip sub-seconds over microsecond precision
70+
$dateTimeValue = DateTimeValue::fromString('2001-10-26T21:32:52.1234567');
71+
$this->assertEquals('2001-10-26T21:32:52.123456', $dateTimeValue->getValue());
72+
73+
// Strip sub-second trailing zero's and make sure the decimal sign is removed
74+
$dateTimeValue = DateTimeValue::fromString('2001-10-26T21:32:52.00Z');
75+
$this->assertEquals('2001-10-26T21:32:52Z', $dateTimeValue->getValue());
76+
77+
// Strip sub-seconds over microsecond precision with timezone
78+
$dateTimeValue = DateTimeValue::fromString('2001-10-26T21:32:52.1234567+01:00');
79+
$this->assertEquals('2001-10-26T21:32:52.123456+01:00', $dateTimeValue->getValue());
80+
81+
// Strip sub-seconds over microsecond precision with timezone Zulu
82+
$dateTimeValue = DateTimeValue::fromString('2001-10-26T21:32:52.1234567Z');
83+
$this->assertEquals('2001-10-26T21:32:52.123456Z', $dateTimeValue->getValue());
84+
}
85+
86+
5787
/**
5888
* @return array<string, array{0: true, 1: string}>
5989
*/
6090
public static function provideValidDateTime(): array
6191
{
6292
return [
6393
'whitespace collapse' => [true, ' 2001-10-26T21:32:52 '],
94+
'trailing sub-second zero' => [true, '2001-10-26T21:32:52.1230'],
95+
'trailing sub-second zero with timezone' => [true, '2001-10-26T21:32:52.1230+00:00'],
96+
'trailing sub-second zero with timezone Zulu' => [true, '2001-10-26T21:32:52.1230Z'],
97+
'all trailing sub-second zero' => [true, '2001-10-26T21:32:52.00'],
98+
'all trailing sub-second zero with timezone' => [true, '2001-10-26T21:32:52.00+00:00'],
99+
'all trailing sub-second zero with timezone Zulu' => [true, '2001-10-26T21:32:52.00Z'],
64100
];
65101
}
66102

tests/XMLSchema/Type/TimeValueTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace SimpleSAML\Test\XMLSchema\Type\Builtin;
66

7+
use DateTimeImmutable;
78
use PHPUnit\Framework\Attributes\CoversClass;
89
use PHPUnit\Framework\Attributes\DataProvider;
910
use PHPUnit\Framework\Attributes\DataProviderExternal;
@@ -40,13 +41,62 @@ public function testTime(bool $shouldPass, string $time): void
4041
}
4142

4243

44+
/**
45+
* Test the fromDateTime function
46+
*/
47+
#[DependsOnClass(TimeTest::class)]
48+
public function testFromDateTime(): void
49+
{
50+
$t = new DateTimeImmutable('00:00:00+00:00');
51+
52+
$timeValue = TimeValue::fromDateTime($t);
53+
$this->assertEquals('00:00:00+00:00', $timeValue->getValue());
54+
}
55+
56+
57+
/**
58+
*/
59+
public function testSubSeconds(): void
60+
{
61+
// Strip sub-second trailing zero's and make sure the decimal sign is removed
62+
$timeValue = TimeValue::fromString('21:32:52.00');
63+
$this->assertEquals('21:32:52', $timeValue->getValue());
64+
65+
// Strip sub-second trailing zero's
66+
$timeValue = TimeValue::fromString('21:32:52.12300');
67+
$this->assertEquals('21:32:52.123', $timeValue->getValue());
68+
69+
// Strip sub-seconds over microsecond precision
70+
$timeValue = TimeValue::fromString('21:32:52.1234567');
71+
$this->assertEquals('21:32:52.123456', $timeValue->getValue());
72+
73+
// Strip sub-second trailing zero's and make sure the decimal sign is removed
74+
$timeValue = TimeValue::fromString('21:32:52.00Z');
75+
$this->assertEquals('21:32:52Z', $timeValue->getValue());
76+
77+
// Strip sub-seconds over microsecond precision with timezone
78+
$timeValue = TimeValue::fromString('21:32:52.1234567+01:00');
79+
$this->assertEquals('21:32:52.123456+01:00', $timeValue->getValue());
80+
81+
// Strip sub-seconds over microsecond precision with timezone Zulu
82+
$timeValue = TimeValue::fromString('21:32:52.1234567Z');
83+
$this->assertEquals('21:32:52.123456Z', $timeValue->getValue());
84+
}
85+
86+
4387
/**
4488
* @return array<string, array{0: true, 1: string}>
4589
*/
4690
public static function provideValidTime(): array
4791
{
4892
return [
4993
'whitespace collapse' => [true, "\n 21:32:52.12679\t "],
94+
'trailing sub-second zero' => [true, '21:32:52.1230'],
95+
'trailing sub-second zero with timezone' => [true, '21:32:52.1230+00:00'],
96+
'trailing sub-second zero with timezone Zulu' => [true, '21:32:52.1230Z'],
97+
'all trailing sub-second all zero' => [true, '21:32:52.00'],
98+
'all trailing sub-second all zero with timezone' => [true, '21:32:52.00+00:00'],
99+
'all trailing sub-second all zero with timezone Zulu' => [true, '21:32:52.00Z'],
50100
];
51101
}
52102

0 commit comments

Comments
 (0)