1
0
mirror of https://github.com/rlanvin/php-rrule.git synced 2025-02-20 09:54:16 +01:00

Strictier RFC parser for dates (DTSTART and UNTIL)

Ref #13
This commit is contained in:
rlanvin 2016-06-30 11:51:47 +03:00
parent e57f6eb0bc
commit 97d860e55e
2 changed files with 168 additions and 5 deletions

View File

@ -571,6 +571,9 @@ class RRule implements RRuleInterface
$string = trim($string);
$dtstart_type = 'date';
$rfc_date_regexp = '/\d{6}(T\d{6})?Z?/'; // a bit loose
foreach ( explode("\n", $string) as $line ) {
$line = trim($line);
if ( strpos($line,':') === false ) {
@ -586,7 +589,7 @@ class RRule implements RRuleInterface
array_splice($tmp,0,1);
foreach ( $tmp as $pair ) {
if ( strpos($pair,'=') === false ) {
throw new \InvalidArgumentException('Failed to parse RFC string, invlaid property parameters: '.$pair);
throw new \InvalidArgumentException('Failed to parse RFC string, invalid property parameters: '.$pair);
}
list($key,$value) = explode('=',$pair);
$property_params[$key] = $value;
@ -595,15 +598,72 @@ class RRule implements RRuleInterface
switch ( $property_name ) {
case 'DTSTART':
$tmp = null;
if ( ! preg_match($rfc_date_regexp, $property_value) ) {
throw new \InvalidArgumentException(
'Invalid DTSTART property: date or date time format incorrect'
);
}
if ( isset($property_params['TZID']) ) {
// TZID must only be specified if this is a date-time (see section 3.3.4 & 3.3.5 of RFC 5545)
if ( strpos($property_value, 'T') === false ) {
throw new \InvalidArgumentException(
'Invalid DTSTART property: TZID should not be specified if there is no time component'
);
}
// The "TZID" property parameter MUST NOT be applied to DATE-TIME
// properties whose time values are specified in UTC.
if ( strpos($property_value, 'Z') !== false ) {
throw new \InvalidArgumentException(
'Invalid DTSTART property: TZID must not be applied when time is specified in UTC'
);
}
$dtstart_type = 'tzid';
$tmp = new \DateTimeZone($property_params['TZID']);
}
elseif ( strpos($property_value, 'T') !== false ) {
if ( strpos($property_value, 'Z') === false ) {
$dtstart_type = 'localtime'; // no timezone
}
else {
$dtstart_type = 'utc';
}
}
$parts['DTSTART'] = new \DateTime($property_value, $tmp);
break;
case 'RRULE':
foreach ( explode(';',$property_value) as $pair ) {
list($key, $value) = explode('=', $pair);
if ( $key === 'UNTIL' ) {
if ( ! preg_match($rfc_date_regexp, $value) ) {
throw new \InvalidArgumentException(
'Invalid DTSTART property: date or date time format incorrect'
);
}
switch ( $dtstart_type ) {
case 'date':
if ( strpos($value, 'T') !== false) {
throw new \InvalidArgumentException(
'Invalid UNTIL property: The value of the UNTIL rule part MUST be a date if DTSTART is a date.'
);
}
break;
case 'localtime':
if ( strpos($value, 'T') === false || strpos($value, 'Z') !== false ) {
throw new \InvalidArgumentException(
'Invalid UNTIL property: if the "DTSTART" property is specified as a date with local time, then the UNTIL rule part MUST also be specified as a date with local time'
);
}
break;
case 'tzid':
case 'utc':
if ( strpos($value, 'T') === false || strpos($value, 'Z') === false ) {
throw new \InvalidArgumentException(
'Invalid UNTIL property: if the "DTSTART" property is specified as a date with UTC time or a date with local time and time zone reference, then the UNTIL rule part MUST be specified as a date with UTC time.'
);
}
break;
}
$value = new \DateTime($value);
}
$parts[$key] = $value;

View File

@ -15,6 +15,8 @@ class RRuleTest extends PHPUnit_Framework_TestCase
{
return array(
array(array()),
array(array('FOOBAR' => 'DAILY')),
array(array('FREQ' => 'foobar')),
array(array('FREQ' => 'DAILY', 'INTERVAL' => -1)),
array(array('FREQ' => 'DAILY', 'UNTIL' => 'foobar')),
@ -1688,7 +1690,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
}
///////////////////////////////////////////////////////////////////////////////
// Other tests
// RFC Strings
public function rfcStrings()
{
@ -1713,8 +1715,14 @@ class RRuleTest extends PHPUnit_Framework_TestCase
RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTHDAY=1,2,5,31,-1,-3,-15;BYSETPOS=-1,1'),
array(' DTSTART;TZID=America/New_York:19970512T090000
RRULE:FREQ=YEARLY;BYWEEKNO=20,30,40;BYDAY=MO'),
array(' DTSTART;TZID=America/New_York:19970512T090000
RRULE:FREQ=YEARLY;BYYEARDAY=1,-1,10,-50;BYDAY=MO')
array('DTSTART;TZID=America/New_York:19970512T090000
RRULE:FREQ=YEARLY;BYYEARDAY=1,-1,10,-50;BYDAY=MO'),
array('DTSTART:19970512T090000Z
RRULE:FREQ=YEARLY'),
array('DTSTART:19970512T090000
RRULE:FREQ=YEARLY'),
array('DTSTART:19970512
RRULE:FREQ=YEARLY'),
);
}
@ -1724,11 +1732,56 @@ class RRuleTest extends PHPUnit_Framework_TestCase
public function testRfcStrings($str)
{
$rule = new RRule($str);
// test that parsing the string produces the same result
// as generating the string from a rule
$this->assertEquals($rule, new RRule($rule->rfcString()));
}
public function invalidRfcStrings()
{
return array(
// test invalid date formats
array('DTSTART:2006-06-24
RRULE:FREQ=DAILY'),
array('DTSTART:2006-06-24 12:00:00
RRULE:FREQ=DAILY'),
array('DTSTART:20060624
RRULE:FREQ=DAILY;UNTIL=2006-06-24'),
// test combinations of DTSTART and UNTIL which are invalid
array('DTSTART;TZID=Australia/Sydney:20160624
RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20160628'),
array('DTSTART;TZID=America/New_York:19970512T090000Z
RRULE:FREQ=YEARLY'),
array('DTSTART;TZID=America/New_York:19970512T090000
RRULE:FREQ=YEARLY;UNTIL=19970512'),
array('DTSTART;TZID=America/New_York:19970512T090000
RRULE:FREQ=YEARLY;UNTIL=19970512T090000'),
array('DTSTART:19970512T090000
RRULE:FREQ=YEARLY;UNTIL=19970512'),
array('DTSTART:19970512T090000Z
RRULE:FREQ=YEARLY;UNTIL=19970512'),
array('DTSTART:19970512
RRULE:FREQ=YEARLY;UNTIL=19970512T090000'),
array('DTSTART:19970512
RRULE:FREQ=YEARLY;UNTIL=19970512T090000Z'),
);
}
/**
* @expectedException InvalidArgumentException
* @dataProvider invalidRfcStrings
*/
public function testInvalidRfcStrings($str)
{
$rule = new RRule($str);
}
///////////////////////////////////////////////////////////////////////////////
// Timezone
public function testTimezoneIsKeptIdentical()
{
$rrule = new RRule(array(
@ -1812,6 +1865,56 @@ class RRuleTest extends PHPUnit_Framework_TestCase
$this->assertTrue($rrule->occursAt(date_create('2015-12-02 07:00:00',new DateTimeZone('UTC'))), 'During winter time, Europe/Helsinki is UTC+2 (uncached)');
}
public function rulesWithMismatchedTimezones()
{
return array(
array(
array('DTSTART' => new DateTime('20160624Z'),'FREQ' => 'DAILY','INTERVAL' => 1,'UNTIL' => '20160628'),
array(
date_create('20160624Z'),
date_create('20160625Z'),
date_create('20160626Z'),
date_create('20160627Z'),
// date_create('20160628Z') // will not return this due to timezone mismatch (unless default timezone is utc)
)
),
array(
array('DTSTART' => new DateTime('20160624Z'),'FREQ' => 'DAILY','INTERVAL' => 1,'UNTIL' => '28-06-2016'),
array(
date_create('20160624Z'),
date_create('20160625Z'),
date_create('20160626Z'),
date_create('20160627Z'),
// date_create('20160628Z') // will not return this due to timezone mismatch (unless default timezone is utc)
)
),
array(
array('DTSTART' => new DateTime('20160624Z'),'FREQ' => 'DAILY','INTERVAL' => 1,'UNTIL' => new DateTime('20160628', new DateTimeZone('Europe/Paris'))),
array(
date_create('20160624Z'),
date_create('20160625Z'),
date_create('20160626Z'),
date_create('20160627Z'),
// date_create('20160628Z') // will not return this due to timezone mismatch (unless default timezone is utc)
)
)
);
}
/**
* Test bug issue #13
* @see https://github.com/rlanvin/php-rrule/issues/13
* @dataProvider rulesWithMismatchedTimezones
*/
public function testRulesWithMismatchedTimezones($rule, $occurrences)
{
$rrule = new RRule($rule);
$this->assertEquals($occurrences, $rrule->getOccurrences(), 'Mismatched timezones makes for strange results');
}
///////////////////////////////////////////////////////////////////////////////
// Other tests
public function testIsFinite()
{
$rrule = new RRule(array(
@ -2084,7 +2187,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
$rrule->humanReadable(array(
'locale' => 'xx',
'fallback' => 'xx'
));
)); // the locales are correctly formatted, but not such file exist, so this should throw a RuntimeException
}
/**