diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 13000f058..bda93aa2a 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -11,7 +11,9 @@ use Icinga\Data\Selectable; use Icinga\Exception\ProgrammingError; use Icinga\Exception\QueryException; use Icinga\Exception\StatementException; +use Icinga\Util\ASN1; use Icinga\Util\StringHelper; +use InvalidArgumentException; /** * Abstract base class for concrete repository implementations @@ -929,6 +931,8 @@ abstract class Repository implements Selectable * @param string|null $value * * @return int + * + * @see https://tools.ietf.org/html/rfc4517#section-3.3.13 */ protected function retrieveGeneralizedTime($value) { @@ -936,20 +940,10 @@ abstract class Repository implements Selectable return $value; } - if (($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false - ) { - return $dateTime->getTimeStamp(); - } else { - Logger::debug(sprintf( - 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".', - $value, - $this->getName() - )); + try { + return ASN1::parseGeneralizedTime($value)->getTimeStamp(); + } catch (InvalidArgumentException $e) { + Logger::debug(sprintf('Repository "%s": %s', $this->getName(), $e->getMessage())); } } diff --git a/library/Icinga/Util/ASN1.php b/library/Icinga/Util/ASN1.php new file mode 100644 index 000000000..9e00258de --- /dev/null +++ b/library/Icinga/Util/ASN1.php @@ -0,0 +1,102 @@ + + [0-9]{4} # century year + (?:0[1-9]|1[0-2]) # month + (?:0[1-9]|[12][0-9]|3[0-1]) # day + (?:[01][0-9]|2[0-3]) # hour + ) + (?: + (?P[0-5][0-9]) # minute + (?P[0-5][0-9]|60)? # second or leap-second + )? + (?:[.,](?P[0-9]+))? # fraction + (?P # g-time-zone + Z + | + [-+] + (?:[01][0-9]|2[0-3]) # hour + (?:[0-5][0-9])? # minute + ) +\z/x +EOD; + + $matches = array(); + + if (preg_match($generalizedTimePattern, $value, $matches)) { + $dateTimeRaw = $matches['YmdH']; + $dateTimeFormat = 'YmdH'; + + if ($matches['i'] !== '') { + $dateTimeRaw .= $matches['i']; + $dateTimeFormat .= 'i'; + + if ($matches['s'] !== '') { + $dateTimeRaw .= $matches['s']; + $dateTimeFormat .= 's'; + $fractionOfSeconds = 1; + } else { + $fractionOfSeconds = 60; + } + } else { + $fractionOfSeconds = 3600; + } + + $dateTimeFormat .= 'O'; + + if ($matches['tz'] === 'Z') { + $dateTimeRaw .= '+0000'; + } else { + $dateTimeRaw .= $matches['tz']; + + if (strlen($matches['tz']) === 3) { + $dateTimeRaw .= '00'; + } + } + + $dateTime = DateTime::createFromFormat($dateTimeFormat, $dateTimeRaw); + + if ($dateTime !== false) { + if (isset($matches['frac'])) { + $dateTime->add(new DateInterval( + 'PT' . round((float) ('0.' . $matches['frac']) * $fractionOfSeconds) . 'S' + )); + } + + return $dateTime; + } + } + + throw new InvalidArgumentException(sprintf( + 'Failed to parse %s based on the ASN.1 standard (GeneralizedTime)', + var_export($value, true) + )); + } +} diff --git a/test/php/library/Icinga/Util/ASN1Test.php b/test/php/library/Icinga/Util/ASN1Test.php new file mode 100644 index 000000000..8608e75a9 --- /dev/null +++ b/test/php/library/Icinga/Util/ASN1Test.php @@ -0,0 +1,102 @@ +assertValidGeneralizedTime("1970010203$is$frac$tz"); + } + } + } + } + + public function testAllGeneralizedTimeRangeBorders() + { + $this->assertBadGeneralizedTime('1970000203Z'); + $this->assertValidGeneralizedTime('1970010203Z'); + $this->assertValidGeneralizedTime('1970120203Z'); + $this->assertBadGeneralizedTime('1970130203Z'); + + $this->assertBadGeneralizedTime('1970010003Z'); + $this->assertValidGeneralizedTime('1970010103Z'); + $this->assertValidGeneralizedTime('1970013103Z'); + $this->assertBadGeneralizedTime('1970013203Z'); + + $this->assertValidGeneralizedTime('1970010200Z'); + $this->assertValidGeneralizedTime('1970010223Z'); + $this->assertBadGeneralizedTime('1970010224Z'); + + $this->assertValidGeneralizedTime('197001020300Z'); + $this->assertValidGeneralizedTime('197001020359Z'); + $this->assertBadGeneralizedTime('197001020360Z'); + + $this->assertValidGeneralizedTime('19700102030400Z'); + $this->assertValidGeneralizedTime('19700102030460Z'); + $this->assertBadGeneralizedTime('19700102030461Z'); + + foreach (array('-', '+') as $sign) { + $this->assertValidGeneralizedTime("1970010203{$sign}00"); + $this->assertValidGeneralizedTime("1970010203{$sign}23"); + $this->assertBadGeneralizedTime("1970010203{$sign}24"); + + $this->assertValidGeneralizedTime("1970010203{$sign}0000"); + $this->assertValidGeneralizedTime("1970010203{$sign}0059"); + $this->assertBadGeneralizedTime("1970010203{$sign}0060"); + } + } + + public function testGeneralizedTimeFractions() + { + $this->assertGeneralizedTimeDiff('19700102030405.9Z', '19700102030405Z', 1); + $this->assertGeneralizedTimeDiff('197001020304.5Z', '197001020304Z', 30); + $this->assertGeneralizedTimeDiff('1970010203.5Z', '1970010203Z', 1800); + } + + protected function assertValidGeneralizedTime($value) + { + try { + $dateTime = ASN1::parseGeneralizedTime($value); + } catch (InvalidArgumentException $e) { + $dateTime = null; + } + + $this->assertInstanceOf( + '\DateTime', + $dateTime, + 'Failed asserting that ' . var_export($value, true) . ' is a valid date/time' + ); + } + + protected function assertBadGeneralizedTime($value) + { + $valid = true; + + try { + ASN1::parseGeneralizedTime($value); + } catch (InvalidArgumentException $e) { + $valid = false; + } + + $this->assertFalse($valid, 'Failed asserting that ' . var_export($value, true) . ' is not a valid date/time'); + } + + protected function assertGeneralizedTimeDiff($lhs, $rhs, $seconds) + { + $this->assertSame( + ASN1::parseGeneralizedTime($lhs)->getTimestamp() - ASN1::parseGeneralizedTime($rhs)->getTimestamp(), + $seconds, + 'Failed asserting that ' . var_export($lhs, true) . ' and ' . var_export($rhs, true) + . " differ by $seconds seconds" + ); + } +}