Sabre VObject v5.0.0

This commit is contained in:
the-djmaze 2026-10-21 15:18:17 +02:00
parent aad651f2a1
commit e66f325aa3
54 changed files with 598 additions and 644 deletions

View file

@ -95,7 +95,7 @@ class BirthdayCalendarGenerator
// We've seen clients (ez-vcard) putting "BDAY:" properties // We've seen clients (ez-vcard) putting "BDAY:" properties
// without a value into vCards. If we come across those, we'll // without a value into vCards. If we come across those, we'll
// skip them. // skip them.
if (empty($object->BDAY->getValue())) { if ('' === $object->BDAY->getValue()) {
continue; continue;
} }
@ -116,7 +116,7 @@ class BirthdayCalendarGenerator
// Skip if we can't parse the BDAY value. // Skip if we can't parse the BDAY value.
try { try {
$dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue()); $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue());
} catch (InvalidDataException $e) { } catch (InvalidDataException) {
continue; continue;
} }

View file

@ -92,7 +92,7 @@ class Cli
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
try { try {
list($options, $positional) = $this->parseArguments($argv); [$options, $positional] = $this->parseArguments($argv);
if (isset($options['q'])) { if (isset($options['q'])) {
$this->quiet = true; $this->quiet = true;
@ -110,27 +110,10 @@ class Cli
return 0; return 0;
case 'format': case 'format':
switch ($value) { $this->format = match ($value) {
// jcard/jcal documents 'jcard', 'jcal', 'vcard21', 'vcard30', 'vcard40', 'icalendar20', 'json', 'mimedir', 'icalendar', 'vcard' => $value,
case 'jcard': default => throw new \InvalidArgumentException('Unknown format: '.$value),
case 'jcal': };
// specific document versions
case 'vcard21':
case 'vcard30':
case 'vcard40':
case 'icalendar20':
// specific formats
case 'json':
case 'mimedir':
// icalendar/vcard
case 'icalendar':
case 'vcard':
$this->format = $value;
break;
default:
throw new \InvalidArgumentException('Unknown format: '.$value);
}
break; break;
case 'pretty': case 'pretty':
$this->pretty = true; $this->pretty = true;
@ -139,28 +122,11 @@ class Cli
$this->forgiving = true; $this->forgiving = true;
break; break;
case 'inputformat': case 'inputformat':
switch ($value) { $this->inputFormat = match ($value) {
// json formats 'jcard', 'jcal', 'json' => 'json',
case 'jcard': 'mimedir', 'icalendar', 'vcard', 'vcard21', 'vcard30', 'vcard40', 'icalendar20' => 'mimedir',
case 'jcal': default => throw new \InvalidArgumentException('Unknown format: '.$value),
case 'json': };
$this->inputFormat = 'json';
break;
// mimedir formats
case 'mimedir':
case 'icalendar':
case 'vcard':
case 'vcard21':
case 'vcard30':
case 'vcard40':
case 'icalendar20':
$this->inputFormat = 'mimedir';
break;
default:
throw new \InvalidArgumentException('Unknown format: '.$value);
}
break; break;
default: default:
throw new \InvalidArgumentException('Unknown option: '.$name); throw new \InvalidArgumentException('Unknown option: '.$name);
@ -181,7 +147,7 @@ class Cli
throw new \InvalidArgumentException('Too many arguments'); throw new \InvalidArgumentException('Too many arguments');
} }
if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'], true)) {
throw new \InvalidArgumentException('Unknown command: '.$positional[0]); throw new \InvalidArgumentException('Unknown command: '.$positional[0]);
} }
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
@ -201,14 +167,14 @@ class Cli
} }
if (null === $this->inputFormat) { if (null === $this->inputFormat) {
if ('.json' === substr($this->inputPath, -5)) { if (str_ends_with($this->inputPath, '.json')) {
$this->inputFormat = 'json'; $this->inputFormat = 'json';
} else { } else {
$this->inputFormat = 'mimedir'; $this->inputFormat = 'mimedir';
} }
} }
if (null === $this->format) { if (null === $this->format) {
if ('.json' === substr($this->outputPath, -5)) { if (str_ends_with($this->outputPath, '.json')) {
$this->format = 'json'; $this->format = 'json';
} else { } else {
$this->format = 'mimedir'; $this->format = 'mimedir';
@ -219,12 +185,13 @@ class Cli
try { try {
while ($input = $this->readInput()) { while ($input = $this->readInput()) {
// @phpstan-ignore method.dynamicName
$returnCode = $this->$command($input); $returnCode = $this->$command($input);
if (0 !== $returnCode) { if (0 !== $returnCode) {
$realCode = $returnCode; $realCode = $returnCode;
} }
} }
} catch (EofException $e) { } catch (EofException) {
// end of file // end of file
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log('Error: '.$e->getMessage(), 'red'); $this->log('Error: '.$e->getMessage(), 'red');
@ -408,7 +375,7 @@ HELP
} }
fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions));
} else { } else {
fwrite($this->stdout, $vObj->serialize()); fwrite($this->stdout, (string) $vObj->serialize());
} }
return 0; return 0;
@ -538,7 +505,7 @@ HELP
$this->cWrite('red', ':'); $this->cWrite('red', ':');
if ($property instanceof Property\Binary) { if ($property instanceof Property\Binary) {
$this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)'); $this->cWrite('default', 'embedded binary stripped. ('.strlen((string) $property->getValue()).' bytes)');
} else { } else {
$parts = $property->getParts(); $parts = $property->getParts();
$first1 = true; $first1 = true;
@ -593,17 +560,17 @@ HELP
$v = $argv[$ii]; $v = $argv[$ii];
if ('--' === substr($v, 0, 2)) { if (str_starts_with((string) $v, '--')) {
// This is a long-form option. // This is a long-form option.
$optionName = substr($v, 2); $optionName = substr((string) $v, 2);
$optionValue = true; $optionValue = true;
if (strpos($optionName, '=')) { if (strpos($optionName, '=')) {
list($optionName, $optionValue) = explode('=', $optionName); [$optionName, $optionValue] = explode('=', $optionName);
} }
$options[$optionName] = $optionValue; $options[$optionName] = $optionValue;
} elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) { } elseif (str_starts_with((string) $v, '-') && strlen((string) $v) > 1) {
// This is a short-form option. // This is a short-form option.
foreach (str_split(substr($v, 1)) as $option) { foreach (str_split(substr((string) $v, 1)) as $option) {
$options[$option] = true; $options[$option] = true;
} }
} else { } else {
@ -628,6 +595,9 @@ HELP
if (!$this->parser) { if (!$this->parser) {
if ('-' !== $this->inputPath) { if ('-' !== $this->inputPath) {
$this->stdin = fopen($this->inputPath, 'r'); $this->stdin = fopen($this->inputPath, 'r');
if (false === $this->stdin) {
throw new InvalidDataException('Cannot open "'.$this->inputPath);
}
} }
if ('mimedir' === $this->inputFormat) { if ('mimedir' === $this->inputFormat) {

View file

@ -140,7 +140,7 @@ class Component extends Node
// If there's no dot in the name, it's an exact property name, // If there's no dot in the name, it's an exact property name,
// we can just wipe out all those properties. // we can just wipe out all those properties.
// //
if (false === strpos($item, '.')) { if (!str_contains($item, '.')) {
unset($this->children[strtoupper($item)]); unset($this->children[strtoupper($item)]);
return; return;
@ -210,8 +210,8 @@ class Component extends Node
{ {
$group = null; $group = null;
$name = strtoupper($name); $name = strtoupper($name);
if (false !== strpos($name, '.')) { if (str_contains($name, '.')) {
list($group, $name) = explode('.', $name, 2); [$group, $name] = explode('.', $name, 2);
} }
if ('' === $name) { if ('' === $name) {
$name = null; $name = null;
@ -222,16 +222,14 @@ class Component extends Node
if (is_null($group)) { if (is_null($group)) {
return $result; return $result;
} else {
// If we have a group filter as well, we need to narrow it down
// more.
return array_filter(
$result,
function ($child) use ($group) {
return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group;
}
);
} }
// If we have a group filter as well, we need to narrow it down
// more.
return array_filter(
$result,
fn ($child) => $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group
);
} }
// If we got to this point, it means there was no 'name' specified for // If we got to this point, it means there was no 'name' specified for
@ -368,7 +366,7 @@ class Component extends Node
$writer->startElement(strtolower($this->name)); $writer->startElement(strtolower($this->name));
if (!empty($properties)) { if ([] !== $properties) {
$writer->startElement('properties'); $writer->startElement('properties');
foreach ($properties as $property) { foreach ($properties as $property) {
@ -378,7 +376,7 @@ class Component extends Node
$writer->endElement(); $writer->endElement();
} }
if (!empty($components)) { if ([] !== $components) {
$writer->startElement('components'); $writer->startElement('components');
foreach ($components as $component) { foreach ($components as $component) {
@ -422,13 +420,12 @@ class Component extends Node
$matches = $this->select($name); $matches = $this->select($name);
if (0 === count($matches)) { if (0 === count($matches)) {
return null; return null;
} else {
$firstMatch = current($matches);
/* @var $firstMatch Property */
$firstMatch->setIterator(new ElementList(array_values($matches)));
return $firstMatch;
} }
$firstMatch = current($matches);
/* @var $firstMatch Property */
$firstMatch->setIterator(new ElementList(array_values($matches)));
return $firstMatch;
} }
/** /**
@ -542,7 +539,7 @@ class Component extends Node
$messages = []; $messages = [];
foreach ($this->children() as $child) { foreach ($this->children() as $child) {
$name = strtoupper($child->name); $name = strtoupper((string) $child->name);
if (!isset($propertyCounters[$name])) { if (!isset($propertyCounters[$name])) {
$propertyCounters[$name] = 1; $propertyCounters[$name] = 1;
} else { } else {

View file

@ -32,9 +32,9 @@ class VAlarm extends VObject\Component
public function getEffectiveTriggerTime(): \DateTimeImmutable public function getEffectiveTriggerTime(): \DateTimeImmutable
{ {
$trigger = $this->TRIGGER; $trigger = $this->TRIGGER;
if (!isset($trigger['VALUE']) || ($trigger['VALUE'] && 'DURATION' === strtoupper($trigger['VALUE']))) { if (!isset($trigger['VALUE']) || ($trigger['VALUE'] && 'DURATION' === strtoupper((string) $trigger['VALUE']))) {
$triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
$related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START'; $related = (isset($trigger['RELATED']) && 'END' === strtoupper($trigger['RELATED'])) ? 'END' : 'START';
/** @var VEvent|VTodo $parentComponent */ /** @var VEvent|VTodo $parentComponent */
$parentComponent = $this->parent; $parentComponent = $this->parent;
@ -96,9 +96,9 @@ class VAlarm extends VObject\Component
} }
return false; return false;
} else {
return $start <= $effectiveTrigger && $end > $effectiveTrigger;
} }
return $start <= $effectiveTrigger && $end > $effectiveTrigger;
} }
/** /**

View file

@ -33,7 +33,7 @@ class VAvailability extends VObject\Component
*/ */
public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool
{ {
list($effectiveStart, $effectiveEnd) = $this->getEffectiveStartEnd(); [$effectiveStart, $effectiveEnd] = $this->getEffectiveStartEnd();
return return
(is_null($effectiveStart) || $start < $effectiveEnd) (is_null($effectiveStart) || $start < $effectiveEnd)

View file

@ -145,6 +145,10 @@ class VCalendar extends VObject\Document
// Additions from draft-daboo-calendar-availability-05 // Additions from draft-daboo-calendar-availability-05
'BUSYTYPE' => Property\Text::class, 'BUSYTYPE' => Property\Text::class,
// RFC 7986 properties
'NAME' => Property\FlatText::class,
'COLOR' => Property\FlatText::class,
]; ];
/** /**
@ -164,7 +168,7 @@ class VCalendar extends VObject\Document
* *
* @param string|null $componentName filter by component name * @param string|null $componentName filter by component name
* *
* @return VObject\Component[] * @return Component[]
*/ */
public function getBaseComponents(?string $componentName = null): array public function getBaseComponents(?string $componentName = null): array
{ {
@ -327,7 +331,7 @@ class VCalendar extends VObject\Document
foreach ($recurringEvents as $events) { foreach ($recurringEvents as $events) {
try { try {
$it = new EventIterator($events, null, $timeZone); $it = new EventIterator($events, null, $timeZone);
} catch (NoInstancesException $e) { } catch (NoInstancesException) {
// This event is recurring, but it doesn't have a single // This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output // instance. We are skipping this event from the output
// entirely. // entirely.
@ -379,6 +383,18 @@ class VCalendar extends VObject\Document
'CALSCALE' => '?', 'CALSCALE' => '?',
'METHOD' => '?', 'METHOD' => '?',
// RFC 7986 calendar properties
'UID' => '?',
'LAST-MODIFIED' => '?',
'URL' => '?',
'REFRESH-INTERVAL' => '?',
'SOURCE' => '?',
'COLOR' => '?',
'NAME' => '*',
'DESCRIPTION' => '*',
'CATEGORIES' => '*',
'IMAGE' => '*',
]; ];
} }
@ -424,7 +440,7 @@ class VCalendar extends VObject\Document
if ($child instanceof Component) { if ($child instanceof Component) {
++$componentsFound; ++$componentsFound;
if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) { if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'], true)) {
continue; continue;
} }
$componentTypes[] = $child->name; $componentTypes[] = $child->name;

View file

@ -132,6 +132,13 @@ class VCard extends VObject\Document
'ORG-DIRECTORY' => VObject\Property\FlatText::class, 'ORG-DIRECTORY' => VObject\Property\FlatText::class,
'X-CRYPTO' => VObject\Property\XCrypto::class, 'X-CRYPTO' => VObject\Property\XCrypto::class,
// rfc9554 properties
'CREATED' => VObject\Property\VCard\TimeStamp::class,
'GRAMGENDER' => VObject\Property\FlatText::class,
'LANGUAGE' => VObject\Property\VCard\LanguageTag::class,
'PRONOUNS' => VObject\Property\FlatText::class,
'SOCIALPROFILE' => VObject\Property\Uri::class,
]; ];
/** /**
@ -326,6 +333,11 @@ class VCard extends VObject\Document
'CALURI' => '*', 'CALURI' => '*',
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'CLIENTPIDMAP' => '*', 'CLIENTPIDMAP' => '*',
'CREATED' => '?',
'GRAMGENDER' => '*',
'LANGUAGE' => '?',
'PRONOUNS' => '*',
'SOCIALPROFILE' => '*',
'EMAIL' => '*', 'EMAIL' => '*',
'FBURL' => '*', 'FBURL' => '*',
'IMPP' => '*', 'IMPP' => '*',
@ -411,6 +423,34 @@ class VCard extends VObject\Document
return null; return null;
} }
/**
* Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
*
* This function will return null if the exact property list does not exist.
*
* For example to get the property of `TEL;TYPE=HOME,CELL`
* you would call `getByTypes('TEL', ['HOME', 'CELL'])`
*
* @param string[] $types
*
* @return \ArrayAccess|array|null
*/
public function getByTypes(string $propertyName, array $types)
{
$types = array_map(strtolower(...), $types);
foreach ($this->select($propertyName) as $field) {
if (isset($field['TYPE'])) {
$parts = array_map(strtolower(...), $field['TYPE']->getParts());
if (!array_diff($types, $parts) && !array_diff($parts, $types)) {
return $field;
}
}
}
return null;
}
/** /**
* This method should return a list of default property values. * This method should return a list of default property values.
*/ */
@ -453,11 +493,9 @@ class VCard extends VObject\Document
$propertiesByGroup = []; $propertiesByGroup = [];
foreach ($this->children() as $property) { foreach ($this->children() as $property) {
$group = $property->group; $group = $property->group ?? '';
if (!isset($propertiesByGroup[$group])) { $propertiesByGroup[$group] ??= [];
$propertiesByGroup[$group] = [];
}
$propertiesByGroup[$group][] = $property; $propertiesByGroup[$group][] = $property;
} }
@ -465,9 +503,9 @@ class VCard extends VObject\Document
$writer->startElement(strtolower($this->name)); $writer->startElement(strtolower($this->name));
foreach ($propertiesByGroup as $group => $properties) { foreach ($propertiesByGroup as $group => $properties) {
if (!empty($group)) { if ('' !== $group) {
$writer->startElement('group'); $writer->startElement('group');
$writer->writeAttribute('name', strtolower($group)); $writer->writeAttribute('name', strtolower((string) $group));
} }
foreach ($properties as $property) { foreach ($properties as $property) {
@ -487,7 +525,7 @@ class VCard extends VObject\Document
} }
} }
if (!empty($group)) { if ('' !== $group) {
$writer->endElement(); $writer->endElement();
} }
} }
@ -503,7 +541,7 @@ class VCard extends VObject\Document
$className = parent::getClassNameForPropertyName($propertyName); $className = parent::getClassNameForPropertyName($propertyName);
// In vCard 4, BINARY no longer exists, and we need URI instead. // In vCard 4, BINARY no longer exists, and we need URI instead.
if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) { if (VObject\Property\Binary::class === $className && self::VCARD40 === $this->getDocumentType()) {
return VObject\Property\Uri::class; return VObject\Property\Uri::class;
} }

View file

@ -41,10 +41,10 @@ class VEvent extends VObject\Component
*/ */
public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool
{ {
if ($this->RRULE) { if ($this->RRULE || $this->RDATE) {
try { try {
$it = new EventIterator($this, null, $start->getTimezone()); $it = new EventIterator($this, null, $start->getTimezone());
} catch (NoInstancesException $e) { } catch (NoInstancesException) {
// If we've caught this exception, there are no instances // If we've caught this exception, there are no instances
// for the event that fall into the specified time-range. // for the event that fall into the specified time-range.
return false; return false;
@ -117,6 +117,7 @@ class VEvent extends VObject\Component
'DTSTART' => $hasMethod ? '?' : '1', 'DTSTART' => $hasMethod ? '?' : '1',
'CLASS' => '?', 'CLASS' => '?',
'CREATED' => '?', 'CREATED' => '?',
'COLOR' => '?',
'DESCRIPTION' => '?', 'DESCRIPTION' => '?',
'GEO' => '?', 'GEO' => '?',
'LAST-MODIFIED' => '?', 'LAST-MODIFIED' => '?',
@ -137,7 +138,9 @@ class VEvent extends VObject\Component
'ATTENDEE' => '*', 'ATTENDEE' => '*',
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'COMMENT' => '*', 'COMMENT' => '*',
'CONFERENCE' => '*',
'CONTACT' => '*', 'CONTACT' => '*',
'IMAGE' => '*',
'EXDATE' => '*', 'EXDATE' => '*',
'REQUEST-STATUS' => '*', 'REQUEST-STATUS' => '*',
'RELATED-TO' => '*', 'RELATED-TO' => '*',

View file

@ -41,7 +41,7 @@ class VFreeBusy extends VObject\Component
// Every period is formatted as [start]/[end]. The start is an // Every period is formatted as [start]/[end]. The start is an
// absolute UTC time, the end may be an absolute UTC time, or // absolute UTC time, the end may be an absolute UTC time, or
// duration (relative) value. // duration (relative) value.
list($busyStart, $busyEnd) = explode('/', $period); [$busyStart, $busyEnd] = explode('/', $period);
$busyStart = VObject\DateTimeParser::parse($busyStart); $busyStart = VObject\DateTimeParser::parse($busyStart);
$busyEnd = VObject\DateTimeParser::parse($busyEnd); $busyEnd = VObject\DateTimeParser::parse($busyEnd);

View file

@ -62,6 +62,7 @@ class VJournal extends VObject\Component
'CLASS' => '?', 'CLASS' => '?',
'CREATED' => '?', 'CREATED' => '?',
'COLOR' => '?',
'DTSTART' => '?', 'DTSTART' => '?',
'LAST-MODIFIED' => '?', 'LAST-MODIFIED' => '?',
'ORGANIZER' => '?', 'ORGANIZER' => '?',
@ -78,6 +79,7 @@ class VJournal extends VObject\Component
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'COMMENT' => '*', 'COMMENT' => '*',
'CONTACT' => '*', 'CONTACT' => '*',
'IMAGE' => '*',
'DESCRIPTION' => '*', 'DESCRIPTION' => '*',
'EXDATE' => '*', 'EXDATE' => '*',
'RELATED-TO' => '*', 'RELATED-TO' => '*',

View file

@ -54,9 +54,9 @@ class VTodo extends VObject\Component
return return
($start < $due || $start <= $dtstart) ($start < $due || $start <= $dtstart)
&& ($end > $dtstart || $end >= $due); && ($end > $dtstart || $end >= $due);
} else {
return $start <= $dtstart && $end > $dtstart;
} }
return $start <= $dtstart && $end > $dtstart;
} }
if ($due) { if ($due) {
return $start < $due && $end >= $due; return $start < $due && $end >= $due;
@ -98,6 +98,7 @@ class VTodo extends VObject\Component
'CLASS' => '?', 'CLASS' => '?',
'COMPLETED' => '?', 'COMPLETED' => '?',
'CREATED' => '?', 'CREATED' => '?',
'COLOR' => '?',
'DESCRIPTION' => '?', 'DESCRIPTION' => '?',
'DTSTART' => '?', 'DTSTART' => '?',
'GEO' => '?', 'GEO' => '?',
@ -120,7 +121,9 @@ class VTodo extends VObject\Component
'ATTENDEE' => '*', 'ATTENDEE' => '*',
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'COMMENT' => '*', 'COMMENT' => '*',
'CONFERENCE' => '*',
'CONTACT' => '*', 'CONTACT' => '*',
'IMAGE' => '*',
'EXDATE' => '*', 'EXDATE' => '*',
'REQUEST-STATUS' => '*', 'REQUEST-STATUS' => '*',
'RELATED-TO' => '*', 'RELATED-TO' => '*',

View file

@ -42,7 +42,7 @@ class DateTimeParser
try { try {
$date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3].' '.$matches[4].':'.$matches[5].':'.$matches[6], $tz); $date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3].' '.$matches[4].':'.$matches[5].':'.$matches[6], $tz);
} catch (\Exception $e) { } catch (\Exception) {
throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt); throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
} }
@ -63,13 +63,11 @@ class DateTimeParser
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date); throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
} }
if (is_null($tz)) { $tz ??= new \DateTimeZone('UTC');
$tz = new \DateTimeZone('UTC');
}
try { try {
$date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3], $tz); $date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3], $tz);
} catch (\Exception $e) { } catch (\Exception) {
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date); throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
} }
@ -205,9 +203,9 @@ class DateTimeParser
return self::parseDuration($date); return self::parseDuration($date);
} elseif (8 === strlen($date)) { } elseif (8 === strlen($date)) {
return self::parseDate($date, $referenceTz); return self::parseDate($date, $referenceTz);
} else {
return self::parseDateTime($date, $referenceTz);
} }
return self::parseDateTime($date, $referenceTz);
} }
/** /**
@ -329,7 +327,7 @@ class DateTimeParser
$result = []; $result = [];
foreach ($parts as $part) { foreach ($parts as $part) {
if (empty($matches[$part])) { if (!array_key_exists($part, $matches) || '' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
} elseif ('-' === $matches[$part] || '--' === $matches[$part]) { } elseif ('-' === $matches[$part] || '--' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
@ -424,7 +422,7 @@ class DateTimeParser
$result = []; $result = [];
foreach ($parts as $part) { foreach ($parts as $part) {
if (empty($matches[$part])) { if (!array_key_exists($part, $matches) || '' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
} elseif ('-' === $matches[$part]) { } elseif ('-' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
@ -541,7 +539,7 @@ class DateTimeParser
$parts['year0'] = &$parts['year']; $parts['year0'] = &$parts['year'];
foreach ($parts as $part => &$value) { foreach ($parts as $part => &$value) {
if (!empty($matches[$part])) { if (array_key_exists($part, $matches) && '' !== $matches[$part]) {
$value = $matches[$part]; $value = $matches[$part];
} }
} }

View file

@ -118,10 +118,10 @@ abstract class Document extends Component
public function create(string $name) public function create(string $name)
{ {
if (isset(static::$componentMap[strtoupper($name)])) { if (isset(static::$componentMap[strtoupper($name)])) {
return call_user_func_array([$this, 'createComponent'], func_get_args()); return $this->createComponent(...func_get_args());
} else {
return call_user_func_array([$this, 'createProperty'], func_get_args());
} }
return $this->createProperty(...func_get_args());
} }
/** /**
@ -146,9 +146,7 @@ abstract class Document extends Component
if (isset(static::$componentMap[$name])) { if (isset(static::$componentMap[$name])) {
$class = static::$componentMap[$name]; $class = static::$componentMap[$name];
} }
if (is_null($children)) { $children ??= [];
$children = [];
}
return new $class($this, $name, $children, $defaults); return new $class($this, $name, $children, $defaults);
} }
@ -180,26 +178,29 @@ abstract class Document extends Component
$class = null; $class = null;
// If a VALUE parameter is supplied, we have to use that
// According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
// If the property's value is the default value type, then this
// parameter need not be specified. However, if the property's
// default value type is overridden by some other allowable value
// type, then this parameter MUST be specified.
if (!$valueType) {
$valueType = $parameters['VALUE'] ?? null;
}
if ($valueType) { if ($valueType) {
// The valueType argument comes first to figure out the correct // The valueType argument comes first to figure out the correct
// class. // class.
$class = $this->getClassNameForPropertyValue($valueType); $class = $this->getClassNameForPropertyValue($valueType);
} }
if (is_null($class)) { // If the value parameter is not set or set to something we do not recognize
// If a VALUE parameter is supplied, we should use that. // we do not attempt to interpret or parse the datass value as specified in
if (isset($parameters['VALUE'])) { // https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
$class = $this->getClassNameForPropertyValue($parameters['VALUE']); // So when we so far did not get a class-name, we use the default for the property
if (is_null($class)) { $class ??= $this->getClassNameForPropertyName($name);
throw new InvalidDataException('Unsupported VALUE parameter for '.$name.' property. You supplied "'.$parameters['VALUE'].'"');
} $parameters ??= [];
} else {
$class = $this->getClassNameForPropertyName($name);
}
}
if (is_null($parameters)) {
$parameters = [];
}
return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString); return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString);
} }

View file

@ -11,25 +11,19 @@ namespace Sabre\VObject;
*/ */
class FreeBusyData class FreeBusyData
{ {
/**
* Start timestamp.
*/
protected int $start;
/**
* End timestamp.
*/
protected int $end;
/** /**
* A list of free-busy times. * A list of free-busy times.
*/ */
protected array $data; protected array $data;
public function __construct(int $start, int $end) public function __construct(/**
* Start timestamp.
*/
protected int $start, /**
* End timestamp.
*/
protected int $end)
{ {
$this->start = $start;
$this->end = $end;
$this->data = []; $this->data = [];
$this->data[] = [ $this->data[] = [

View file

@ -79,9 +79,7 @@ class FreeBusyGenerator
if ($objects) { if ($objects) {
$this->setObjects($objects); $this->setObjects($objects);
} }
if (is_null($timeZone)) { $timeZone ??= new \DateTimeZone('UTC');
$timeZone = new \DateTimeZone('UTC');
}
$this->setTimeZone($timeZone); $this->setTimeZone($timeZone);
} }
@ -217,7 +215,7 @@ class FreeBusyGenerator
$new = []; $new = [];
foreach ($old as $vavail) { foreach ($old as $vavail) {
list($compStart, $compEnd) = $vavail->getEffectiveStartEnd(); [$compStart, $compEnd] = $vavail->getEffectiveStartEnd();
// We don't care about date-times that are earlier or later than the // We don't care about date-times that are earlier or later than the
// start and end of the freebusy report, so this gets normalized // start and end of the freebusy report, so this gets normalized
@ -237,7 +235,7 @@ class FreeBusyGenerator
// Going through our existing list of components to see if there's // Going through our existing list of components to see if there's
// a higher priority component that already fully covers this one. // a higher priority component that already fully covers this one.
foreach ($new as $higherVavail) { foreach ($new as $higherVavail) {
list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd(); [$higherStart, $higherEnd] = $higherVavail->getEffectiveStartEnd();
if ( if (
(is_null($higherStart) || $higherStart < $compStart) (is_null($higherStart) || $higherStart < $compStart)
&& (is_null($higherEnd) || $higherEnd > $compEnd) && (is_null($higherEnd) || $higherEnd > $compEnd)
@ -259,7 +257,7 @@ class FreeBusyGenerator
// priority components to override the lower ones. // priority components to override the lower ones.
foreach (array_reverse($new) as $vavail) { foreach (array_reverse($new) as $vavail) {
$busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE'; $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE';
list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd(); [$vavailStart, $vavailEnd] = $vavail->getEffectiveStartEnd();
// Making the component size no larger than the requested free-busy // Making the component size no larger than the requested free-busy
// report range. // report range.
@ -281,7 +279,7 @@ class FreeBusyGenerator
// Looping over the AVAILABLE components. // Looping over the AVAILABLE components.
if (isset($vavail->AVAILABLE)) { if (isset($vavail->AVAILABLE)) {
foreach ($vavail->AVAILABLE as $available) { foreach ($vavail->AVAILABLE as $available) {
list($availStart, $availEnd) = $available->getEffectiveStartEnd(); [$availStart, $availEnd] = $available->getEffectiveStartEnd();
$fbData->add( $fbData->add(
$availStart->getTimeStamp(), $availStart->getTimeStamp(),
$availEnd->getTimeStamp(), $availEnd->getTimeStamp(),
@ -362,7 +360,7 @@ class FreeBusyGenerator
if ($component->RRULE) { if ($component->RRULE) {
try { try {
$iterator = new EventIterator($object, (string) $component->UID, $this->timeZone); $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone);
} catch (NoInstancesException $e) { } catch (NoInstancesException) {
// This event is recurring, but it doesn't have a single // This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output // instance. We are skipping this event from the output
// entirely. // entirely.
@ -437,12 +435,12 @@ class FreeBusyGenerator
continue; continue;
} }
$values = explode(',', $freebusy); $values = explode(',', (string) $freebusy);
foreach ($values as $value) { foreach ($values as $value) {
list($startTime, $endTime) = explode('/', $value); [$startTime, $endTime] = explode('/', $value);
$startTime = DateTimeParser::parseDateTime($startTime); $startTime = DateTimeParser::parseDateTime($startTime);
if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) { if (str_starts_with($endTime, 'P') || str_starts_with($endTime, '-P')) {
$duration = DateTimeParser::parseDuration($endTime); $duration = DateTimeParser::parseDuration($endTime);
$endTime = clone $startTime; $endTime = clone $startTime;
$endTime = $endTime->add($duration); $endTime = $endTime->add($duration);
@ -507,7 +505,7 @@ class FreeBusyGenerator
$vfreebusy->add($dtstamp); $vfreebusy->add($dtstamp);
foreach ($fbData->getData() as $busyTime) { foreach ($fbData->getData() as $busyTime) {
$busyType = strtoupper($busyTime['type']); $busyType = strtoupper((string) $busyTime['type']);
// Ignoring all the FREE parts, because those are already assumed. // Ignoring all the FREE parts, because those are already assumed.
if ('FREE' === $busyType) { if ('FREE' === $busyType) {

View file

@ -228,7 +228,7 @@ class Broker
$eventInfo = $oldEventInfo; $eventInfo = $oldEventInfo;
if (in_array($eventInfo['organizer'], $userHref)) { if (in_array($eventInfo['organizer'], $userHref, true)) {
// This is an organizer deleting the event. // This is an organizer deleting the event.
$eventInfo['attendees'] = []; $eventInfo['attendees'] = [];
// Increasing the sequence, but only if the organizer deleted // Increasing the sequence, but only if the organizer deleted
@ -237,7 +237,7 @@ class Broker
} else { } else {
// This is an attendee deleting the event. // This is an attendee deleting the event.
foreach ($eventInfo['attendees'] as $key => $attendee) { foreach ($eventInfo['attendees'] as $key => $attendee) {
if (in_array($attendee['href'], $userHref)) { if (in_array($attendee['href'], $userHref, true)) {
$eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'], $eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'],
]; ];
} }
@ -246,16 +246,29 @@ class Broker
$baseCalendar = $oldCalendar; $baseCalendar = $oldCalendar;
} }
if (in_array($eventInfo['organizer'], $userHref)) { // Check if the user is the organizer
if (in_array($eventInfo['organizer'], $userHref, true)) {
return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo);
} elseif ($oldCalendar) { }
// We need to figure out if the user is an attendee, but we're only
// doing so if there's an oldCalendar, because we only want to // Check if the user is an attendee
// process updates, not creation of new events. foreach ($eventInfo['attendees'] as $attendee) {
foreach ($eventInfo['attendees'] as $attendee) { if (in_array($attendee['href'], $userHref, true)) {
if (in_array($attendee['href'], $userHref)) { // If this is a event update, we always generate a reply
if ($oldCalendar) {
return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
} }
// If this is a new event, we only generate a reply if the participation status is set
foreach ($attendee['instances'] as $instance) {
if (isset($instance['partstat']) && 'NEEDS-ACTION' !== $instance['partstat']) {
// Attendee has responded (ACCEPTED/DECLINED/TENTATIVE) - generate REPLY
return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
}
}
// User is attendee but no response to process
break;
} }
} }
@ -341,7 +354,7 @@ class Broker
$instances[$recurId] = $attendee['PARTSTAT']->getValue(); $instances[$recurId] = $attendee['PARTSTAT']->getValue();
if (isset($vevent->{'REQUEST-STATUS'})) { if (isset($vevent->{'REQUEST-STATUS'})) {
$requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
list($requestStatus) = explode(';', $requestStatus); [$requestStatus] = explode(';', $requestStatus);
} }
} }
@ -479,7 +492,6 @@ class Broker
} }
$messages = []; $messages = [];
foreach ($attendees as $attendee) { foreach ($attendees as $attendee) {
// An organizer can also be an attendee. We should not generate any // An organizer can also be an attendee. We should not generate any
// messages for those. // messages for those.
@ -554,7 +566,7 @@ class Broker
$message->significantChange = $message->significantChange =
'REQUEST' === $attendee['forceSend'] 'REQUEST' === $attendee['forceSend']
|| count($oldAttendeeInstances) != count($newAttendeeInstances) || count($oldAttendeeInstances) !== count($newAttendeeInstances)
|| count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 || count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0
|| $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
@ -564,9 +576,9 @@ class Broker
// We need to find a list of events that the attendee // We need to find a list of events that the attendee
// is not a part of to add to the list of exceptions. // is not a part of to add to the list of exceptions.
$exceptions = []; $exceptions = [];
foreach ($eventInfo['instances'] as $instanceId => $vevent) { foreach ($eventInfo['instances'] as $eventInstanceId => $vevent) {
if (!isset($attendee['newInstances'][$instanceId])) { if (!isset($attendee['newInstances'][$eventInstanceId])) {
$exceptions[] = $instanceId; $exceptions[] = $eventInstanceId;
} }
} }
@ -580,6 +592,9 @@ class Broker
)); ));
} else { } else {
$currentEvent->EXDATE = $exceptions; $currentEvent->EXDATE = $exceptions;
if ($currentEvent->DTSTART['TZID']) {
$currentEvent->EXDATE['TZID'] = clone $currentEvent->DTSTART['TZID'];
}
} }
} }
@ -588,15 +603,13 @@ class Broker
unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
foreach ($currentEvent->ATTENDEE as $attendee) { foreach ($currentEvent->ATTENDEE as $currentEventAttendee) {
unset($attendee['SCHEDULE-FORCE-SEND']); unset($currentEventAttendee['SCHEDULE-FORCE-SEND']);
unset($attendee['SCHEDULE-STATUS']); unset($currentEventAttendee['SCHEDULE-STATUS']);
// We're adding PARTSTAT=NEEDS-ACTION to ensure that // We're adding PARTSTAT=NEEDS-ACTION to ensure that
// iOS shows an "Inbox Item" // iOS shows an "Inbox Item"
if (!isset($attendee['PARTSTAT'])) { $currentEventAttendee['PARTSTAT'] ??= 'NEEDS-ACTION';
$attendee['PARTSTAT'] = 'NEEDS-ACTION';
}
} }
} }
@ -633,7 +646,12 @@ class Broker
return []; return [];
} }
$oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? $oldInstances =
is_array($oldEventInfo['attendees'])
&& array_key_exists($attendee, $oldEventInfo['attendees'])
&& is_array($oldEventInfo['attendees'][$attendee])
&& array_key_exists('instances', $oldEventInfo['attendees'][$attendee])
&& is_array($oldEventInfo['attendees'][$attendee]['instances']) ?
$oldEventInfo['attendees'][$attendee]['instances'] : $oldEventInfo['attendees'][$attendee]['instances'] :
[]; [];
@ -664,7 +682,7 @@ class Broker
// We only need to do that though, if the master event is not declined. // We only need to do that though, if the master event is not declined.
if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) {
foreach ($eventInfo['exdate'] as $exDate) { foreach ($eventInfo['exdate'] as $exDate) {
if (!in_array($exDate, $oldEventInfo['exdate'])) { if (!in_array($exDate, $oldEventInfo['exdate'] ?? [], true)) {
if (isset($instances[$exDate])) { if (isset($instances[$exDate])) {
$instances[$exDate]['newstatus'] = 'DECLINED'; $instances[$exDate]['newstatus'] = 'DECLINED';
} else { } else {
@ -707,7 +725,7 @@ class Broker
$hasReply = false; $hasReply = false;
foreach ($instances as $instance) { foreach ($instances as $instance) {
if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) { if ($instance['oldstatus'] === $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) {
// Skip // Skip
continue; continue;
} }
@ -739,7 +757,7 @@ class Broker
// EXDATE // EXDATE
$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
// Treat is as a DATE field // Treat is as a DATE field
if (strlen($instance['id']) <= 8) { if (strlen((string) $instance['id']) <= 8) {
$event->add('DTSTART', $dt, ['VALUE' => 'DATE']); $event->add('DTSTART', $dt, ['VALUE' => 'DATE']);
} else { } else {
$event->add('DTSTART', $dt); $event->add('DTSTART', $dt);
@ -751,7 +769,7 @@ class Broker
if ('master' !== $instance['id']) { if ('master' !== $instance['id']) {
$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
// Treat is as a DATE field // Treat is as a DATE field
if (strlen($instance['id']) <= 8) { if (strlen((string) $instance['id']) <= 8) {
$event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']); $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']);
} else { } else {
$event->add('RECURRENCE-ID', $dt); $event->add('RECURRENCE-ID', $dt);
@ -774,9 +792,9 @@ class Broker
$message->message = $icalMsg; $message->message = $icalMsg;
return [$message]; return [$message];
} else {
return [];
} }
return [];
} }
/** /**
@ -843,7 +861,7 @@ class Broker
$organizer = $vevent->ORGANIZER->getNormalizedValue(); $organizer = $vevent->ORGANIZER->getNormalizedValue();
$organizerName = $vevent->ORGANIZER['CN'] ?? null; $organizerName = $vevent->ORGANIZER['CN'] ?? null;
} else { } else {
if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) { if (strtoupper($organizer) !== strtoupper((string) $vevent->ORGANIZER->getNormalizedValue())) {
throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.');
} }
} }
@ -869,7 +887,7 @@ class Broker
foreach ($vevent->select('RRULE') as $rr) { foreach ($vevent->select('RRULE') as $rr) {
foreach ($rr->getParts() as $key => $val) { foreach ($rr->getParts() as $key => $val) {
// ignore default values (https://github.com/sabre-io/vobject/issues/126) // ignore default values (https://github.com/sabre-io/vobject/issues/126)
if ('INTERVAL' === $key && 1 == $val) { if ('INTERVAL' === $key && '1' === (string) $val) {
continue; continue;
} }
if (is_array($val)) { if (is_array($val)) {
@ -881,7 +899,7 @@ class Broker
sort($rrule); sort($rrule);
} }
if (isset($vevent->STATUS)) { if (isset($vevent->STATUS)) {
$status = strtoupper($vevent->STATUS->getValue()); $status = strtoupper((string) $vevent->STATUS->getValue());
} }
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
@ -892,11 +910,14 @@ class Broker
$timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone();
} }
} }
$instances[$recurId] = $vevent;
if (isset($vevent->ATTENDEE)) { if (isset($vevent->ATTENDEE)) {
foreach ($vevent->ATTENDEE as $attendee) { foreach ($vevent->ATTENDEE as $attendee) {
if ($this->scheduleAgentServerRules if ($this->scheduleAgentServerRules
&& isset($attendee['SCHEDULE-AGENT']) && isset($attendee['SCHEDULE-AGENT'])
&& 'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue()) && 'CLIENT' === strtoupper((string) $attendee['SCHEDULE-AGENT']->getValue())
) { ) {
continue; continue;
} }
@ -930,10 +951,10 @@ class Broker
]; ];
} }
} }
$instances[$recurId] = $vevent;
} }
foreach ($this->significantChangeProperties as $prop) { foreach ($this->significantChangeProperties as $prop) {
// @phpstan-ignore property.dynamicName
if (isset($vevent->$prop)) { if (isset($vevent->$prop)) {
$propertyValues = $vevent->select($prop); $propertyValues = $vevent->select($prop);

View file

@ -32,12 +32,12 @@ class Message
* Contains the ITip method, which is something like REQUEST, REPLY or * Contains the ITip method, which is something like REQUEST, REPLY or
* CANCEL. * CANCEL.
*/ */
public ?string $method; public ?string $method = null;
/** /**
* The current sequence number for the event. * The current sequence number for the event.
*/ */
public ?int $sequence; public ?int $sequence = null;
/** /**
* The senders' email address. * The senders' email address.
@ -52,7 +52,7 @@ class Message
* The name of the sender. This is often populated from a CN parameter from * The name of the sender. This is often populated from a CN parameter from
* either the ORGANIZER or ATTENDEE, depending on the message. * either the ORGANIZER or ATTENDEE, depending on the message.
*/ */
public ?string $senderName; public ?string $senderName = null;
/** /**
* The recipient's email address. * The recipient's email address.
@ -63,7 +63,7 @@ class Message
* The name of the recipient. This is usually populated with the CN * The name of the recipient. This is usually populated with the CN
* parameter from the ATTENDEE or ORGANIZER property, if it's available. * parameter from the ATTENDEE or ORGANIZER property, if it's available.
*/ */
public ?string $recipientName; public ?string $recipientName = null;
/** /**
* After the message has been delivered, this should contain a string such * After the message has been delivered, this should contain a string such
@ -107,10 +107,9 @@ class Message
{ {
if (!$this->scheduleStatus) { if (!$this->scheduleStatus) {
return false; return false;
} else {
list($scheduleStatus) = explode(';', $this->scheduleStatus);
return $scheduleStatus;
} }
[$scheduleStatus] = explode(';', $this->scheduleStatus);
return $scheduleStatus;
} }
} }

View file

@ -42,7 +42,7 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable, \Js
/** /**
* Reference to the parent object, if this is not the top object. * Reference to the parent object, if this is not the top object.
*/ */
public ?Node $parent; public ?Node $parent = null;
/** /**
* Iterator override. * Iterator override.
@ -52,7 +52,7 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable, \Js
/** /**
* The root document. * The root document.
*/ */
protected ?Component $root; protected ?Component $root = null;
/** /**
* Serializes the node into a mimedir format. * Serializes the node into a mimedir format.

View file

@ -41,7 +41,7 @@ trait PHPUnitAssertions
$input = Reader::read($input); $input = Reader::read($input);
} }
if (!$input instanceof Component) { if (!$input instanceof Component) {
$this->fail('Input must be a string, stream or VObject component'); self::fail('Input must be a string, stream or VObject component');
} }
unset($input->PRODID); unset($input->PRODID);
if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) { if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) {
@ -51,23 +51,28 @@ trait PHPUnitAssertions
return $input; return $input;
}; };
$expected = $getObj($expected)->serialize(); /**
$actual = $getObj($actual)->serialize(); * @var string $expectedSerialized
*/
$expectedSerialized = $getObj($expected)->serialize();
$actualSerialized = $getObj($actual)->serialize();
// Finding wildcards in expected. // Finding wildcards in expected.
preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER); $result = preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expectedSerialized, $matches, PREG_SET_ORDER);
self::assertNotFalse($result);
foreach ($matches as $match) { foreach ($matches as $match) {
$actual = preg_replace( $actualSerialized = preg_replace(
'|^'.preg_quote($match[1], '|').':(.*)\r$|m', '|^'.preg_quote($match[1], '|').':(.*)\r$|m',
$match[1].':**ANY**'."\r", $match[1].':**ANY**'."\r",
$actual (string) $actualSerialized
); );
} }
$this->assertEquals( self::assertEquals(
$expected, $expectedSerialized,
$actual, $actualSerialized,
$message $message
); );
} }

View file

@ -16,7 +16,7 @@ use Sabre\Xml;
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Parameter extends Node class Parameter extends Node implements \Stringable
{ {
/** /**
* Parameter name. * Parameter name.
@ -74,83 +74,12 @@ class Parameter extends Node
*/ */
public static function guessParameterNameByValue(string $value): string public static function guessParameterNameByValue(string $value): string
{ {
switch (strtoupper($value)) { $name = match (strtoupper($value)) {
// Encodings '7-BIT', 'QUOTED-PRINTABLE', 'BASE64' => 'ENCODING',
case '7-BIT': 'WORK', 'HOME', 'PREF', 'DOM', 'INTL', 'POSTAL', 'PARCEL', 'VOICE', 'FAX', 'MSG', 'CELL', 'PAGER', 'BBS', 'MODEM', 'CAR', 'ISDN', 'VIDEO', 'AOL', 'APPLELINK', 'ATTMAIL', 'CIS', 'EWORLD', 'INTERNET', 'IBMMAIL', 'MCIMAIL', 'POWERSHARE', 'PRODIGY', 'TLX', 'X400', 'GIF', 'CGM', 'WMF', 'BMP', 'DIB', 'PICT', 'TIFF', 'PDF', 'PS', 'JPEG', 'MPEG', 'MPEG2', 'AVI', 'QTIME', 'WAVE', 'PCM', 'AIFF', 'X509', 'PGP' => 'TYPE',
case 'QUOTED-PRINTABLE': 'INLINE', 'URL', 'CONTENT-ID', 'CID' => 'VALUE',
case 'BASE64': default => '',
$name = 'ENCODING'; };
break;
// Common types
case 'WORK':
case 'HOME':
case 'PREF':
// Delivery Label Type
case 'DOM':
case 'INTL':
case 'POSTAL':
case 'PARCEL':
// Telephone types
case 'VOICE':
case 'FAX':
case 'MSG':
case 'CELL':
case 'PAGER':
case 'BBS':
case 'MODEM':
case 'CAR':
case 'ISDN':
case 'VIDEO':
// EMAIL types (lol)
case 'AOL':
case 'APPLELINK':
case 'ATTMAIL':
case 'CIS':
case 'EWORLD':
case 'INTERNET':
case 'IBMMAIL':
case 'MCIMAIL':
case 'POWERSHARE':
case 'PRODIGY':
case 'TLX':
case 'X400':
// Photo / Logo format types
case 'GIF':
case 'CGM':
case 'WMF':
case 'BMP':
case 'DIB':
case 'PICT':
case 'TIFF':
case 'PDF':
case 'PS':
case 'JPEG':
case 'MPEG':
case 'MPEG2':
case 'AVI':
case 'QTIME':
// Sound Digital Audio Type
case 'WAVE':
case 'PCM':
case 'AIFF':
// Key types
case 'X509':
case 'PGP':
$name = 'TYPE';
break;
// Value types
case 'INLINE':
case 'URL':
case 'CONTENT-ID':
case 'CID':
$name = 'VALUE';
break;
default:
$name = '';
}
return $name; return $name;
} }
@ -177,9 +106,9 @@ class Parameter extends Node
{ {
if (is_array($this->value)) { if (is_array($this->value)) {
return implode(',', $this->value); return implode(',', $this->value);
} else {
return $this->value;
} }
return $this->value;
} }
/** /**
@ -201,9 +130,9 @@ class Parameter extends Node
return $this->value; return $this->value;
} elseif (is_null($this->value)) { } elseif (is_null($this->value)) {
return []; return [];
} else {
return [$this->value];
} }
return [$this->value];
} }
/** /**
@ -234,7 +163,8 @@ class Parameter extends Node
{ {
return in_array( return in_array(
strtolower($value), strtolower($value),
array_map('strtolower', (array) $this->value) array_map(strtolower(...), (array) $this->value),
true
); );
} }
@ -278,20 +208,19 @@ class Parameter extends Node
// added + as well. // added + as well.
if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) {
return $out.$item; return $out.$item;
} else {
// Enclosing in double-quotes, and using RFC6868 for encoding any
// special characters
$out .= '"'.strtr(
$item,
[
'^' => '^^',
"\n" => '^n',
'"' => '^\'',
]
).'"';
return $out;
} }
// Enclosing in double-quotes, and using RFC6868 for encoding any
// special characters
$out .= '"'.strtr(
$item,
[
'^' => '^^',
"\n" => '^n',
'"' => '^\'',
]
).'"';
return $out;
} }
); );
} }

View file

@ -27,12 +27,12 @@ class Json extends Parser
/** /**
* The input data. * The input data.
*/ */
protected ?array $input; protected ?array $input = null;
/** /**
* Root component. * Root component.
*/ */
protected ?Document $root; protected ?Document $root = null;
/** /**
* This method starts the parsing process. * This method starts the parsing process.
@ -61,16 +61,11 @@ class Json extends Parser
$this->options = $options; $this->options = $options;
} }
switch ($this->input[0]) { $this->root = match ($this->input[0]) {
case 'vcalendar': 'vcalendar' => new VCalendar([], false),
$this->root = new VCalendar([], false); 'vcard' => new VCard([], false),
break; default => throw new ParseException('The root component must either be a vcalendar, or a vcard'),
case 'vcard': };
$this->root = new VCard([], false);
break;
default:
throw new ParseException('The root component must either be a vcalendar, or a vcard');
}
foreach ($this->input[1] as $prop) { foreach ($this->input[1] as $prop) {
$this->root->add($this->parseProperty($prop)); $this->root->add($this->parseProperty($prop));
} }
@ -97,17 +92,13 @@ class Json extends Parser
$self = $this; $self = $this;
$properties = array_map( $properties = array_map(
function ($jProp) use ($self) { $self->parseProperty(...),
return $self->parseProperty($jProp);
},
$jComp[1] $jComp[1]
); );
if (isset($jComp[2])) { if (isset($jComp[2])) {
$components = array_map( $components = array_map(
function ($jComp) use ($self) { $self->parseComponent(...),
return $self->parseComponent($jComp);
},
$jComp[2] $jComp[2]
); );
} else { } else {
@ -134,7 +125,7 @@ class Json extends Parser
$valueType, $valueType,
) = $jProp; ) = $jProp;
$propertyName = strtoupper($propertyName); $propertyName = strtoupper((string) $propertyName);
// This is the default class we would be using if we didn't know the // This is the default class we would be using if we didn't know the
// value type. We're using this value later in this function. // value type. We're using this value later in this function.
@ -144,7 +135,7 @@ class Json extends Parser
$value = array_slice($jProp, 3); $value = array_slice($jProp, 3);
$valueType = strtoupper($valueType); $valueType = strtoupper((string) $valueType);
if (isset($parameters['group'])) { if (isset($parameters['group'])) {
$propertyName = $parameters['group'].'.'.$propertyName; $propertyName = $parameters['group'].'.'.$propertyName;
@ -169,7 +160,7 @@ class Json extends Parser
// If the value type we received (e.g.: TEXT) was not the default value // If the value type we received (e.g.: TEXT) was not the default value
// type for the given property (e.g.: BDAY), we need to add a VALUE= // type for the given property (e.g.: BDAY), we need to add a VALUE=
// parameter. // parameter.
if ($defaultPropertyClass !== get_class($prop)) { if ($defaultPropertyClass !== $prop::class) {
$prop['VALUE'] = $valueType; $prop['VALUE'] = $valueType;
} }

View file

@ -26,6 +26,11 @@ use Sabre\VObject\Property;
*/ */
class MimeDir extends Parser class MimeDir extends Parser
{ {
public const TOKEN_PROPNAME = 1;
public const TOKEN_PROPVALUE = 2;
public const TOKEN_PARAMNAME = 3;
public const TOKEN_PARAMVALUE = 4;
/** /**
* The input stream. * The input stream.
* *
@ -36,7 +41,7 @@ class MimeDir extends Parser
/** /**
* Root component. * Root component.
*/ */
protected ?Document $root; protected ?Document $root = null;
/** /**
* By default, all input will be assumed to be UTF-8. * By default, all input will be assumed to be UTF-8.
@ -106,7 +111,7 @@ class MimeDir extends Parser
*/ */
public function setCharset(string $charset): void public function setCharset(string $charset): void
{ {
if (!in_array($charset, self::$SUPPORTED_CHARSETS)) { if (!in_array($charset, self::$SUPPORTED_CHARSETS, true)) {
throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')'); throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')');
} }
$this->charset = $charset; $this->charset = $charset;
@ -150,23 +155,18 @@ class MimeDir extends Parser
// BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF). // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
// It's 0xEF 0xBB 0xBF in UTF-8 hex. // It's 0xEF 0xBB 0xBF in UTF-8 hex.
if (3 <= strlen($line) if (3 <= strlen((string) $line)
&& 0xEF === ord($line[0]) && 0xEF === ord($line[0])
&& 0xBB === ord($line[1]) && 0xBB === ord($line[1])
&& 0xBF === ord($line[2])) { && 0xBF === ord($line[2])) {
$line = \substr($line, 3); $line = \substr((string) $line, 3);
} }
switch (strtoupper($line)) { $class = match (strtoupper((string) $line)) {
case 'BEGIN:VCALENDAR': 'BEGIN:VCALENDAR' => VCalendar::$componentMap['VCALENDAR'],
$class = VCalendar::$componentMap['VCALENDAR']; 'BEGIN:VCARD' => VCard::$componentMap['VCARD'],
break; default => throw new ParseException('This parser only supports VCARD and VCALENDAR files'),
case 'BEGIN:VCARD': };
$class = VCard::$componentMap['VCARD'];
break;
default:
throw new ParseException('This parser only supports VCARD and VCALENDAR files');
}
$this->root = new $class([], false); $this->root = new $class([], false);
@ -174,10 +174,10 @@ class MimeDir extends Parser
// Reading until we hit END: // Reading until we hit END:
try { try {
$line = $this->readLine(); $line = $this->readLine();
} catch (EofException $oEx) { } catch (EofException) {
$line = 'END:'.$this->root->name; $line = 'END:'.$this->root->name;
} }
if ('END:' === strtoupper(\substr($line, 0, 4))) { if ('END:' === strtoupper(\substr((string) $line, 0, 4))) {
break; break;
} }
$result = $this->parseLine($line); $result = $this->parseLine($line);
@ -186,7 +186,7 @@ class MimeDir extends Parser
} }
} }
$name = strtoupper(\substr($line, 4)); $name = strtoupper(\substr((string) $line, 4));
if ($name !== $this->root->name) { if ($name !== $this->root->name) {
throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"'); throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"');
} }
@ -215,7 +215,7 @@ class MimeDir extends Parser
while (true) { while (true) {
// Reading until we hit END: // Reading until we hit END:
$line = $this->readLine(); $line = $this->readLine();
if ('END:' === strtoupper(\substr($line, 0, 4))) { if ('END:' === strtoupper(\substr((string) $line, 0, 4))) {
break; break;
} }
$result = $this->parseLine($line); $result = $this->parseLine($line);
@ -224,22 +224,21 @@ class MimeDir extends Parser
} }
} }
$name = strtoupper(\substr($line, 4)); $name = strtoupper(\substr((string) $line, 4));
if ($name !== $component->name) { if ($name !== $component->name) {
throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"'); throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"');
} }
return $component; return $component;
} else {
// Property reader
$property = $this->readProperty($line);
if (!$property) {
// Ignored line
return false;
}
return $property;
} }
// Property reader
$property = $this->readProperty($line);
if (!$property) {
// Ignored line
return false;
}
return $property;
} }
/** /**
@ -360,6 +359,12 @@ class MimeDir extends Parser
'value' => null, 'value' => null,
]; ];
/*
* Keep track on the last token we parsed in order to do
* better error checking
*/
$lastToken = null;
$lastParam = null; $lastParam = null;
/* /*
@ -385,10 +390,16 @@ class MimeDir extends Parser
// option is set to ignore invalid lines, we ignore this line // option is set to ignore invalid lines, we ignore this line
// This can happen when servers provide faulty data as iCloud // This can happen when servers provide faulty data as iCloud
// frequently does with X-APPLE-STRUCTURED-LOCATION // frequently does with X-APPLE-STRUCTURED-LOCATION
$lastToken = self::TOKEN_PARAMVALUE;
continue; continue;
} }
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
} }
if ('=' === $match[0][0] && self::TOKEN_PARAMNAME !== $lastToken) {
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.': Missing parameter name for parameter value "'.$match['paramValue'].'"');
}
if (\is_null($property['parameters'][$lastParam])) { if (\is_null($property['parameters'][$lastParam])) {
$property['parameters'][$lastParam] = $value; $property['parameters'][$lastParam] = $value;
} elseif (is_array($property['parameters'][$lastParam])) { } elseif (is_array($property['parameters'][$lastParam])) {
@ -402,21 +413,23 @@ class MimeDir extends Parser
$value, $value,
]; ];
} }
$lastToken = self::TOKEN_PARAMVALUE;
continue; continue;
} }
if (isset($match['paramName'])) { if (isset($match['paramName'])) {
$lastParam = strtoupper($match['paramName']); $lastParam = strtoupper($match['paramName']);
if (!isset($property['parameters'][$lastParam])) { $property['parameters'][$lastParam] ??= null;
$property['parameters'][$lastParam] = null; $lastToken = self::TOKEN_PARAMNAME;
}
continue; continue;
} }
if (isset($match['propValue'])) { if (isset($match['propValue'])) {
$property['value'] = $match['propValue']; $property['value'] = $match['propValue'];
$lastToken = self::TOKEN_PROPVALUE;
continue; continue;
} }
if (isset($match['name']) && $match['name']) { if (isset($match['name']) && 0 < strlen($match['name'])) {
$property['name'] = strtoupper($match['name']); $property['name'] = strtoupper($match['name']);
$lastToken = self::TOKEN_PROPNAME;
continue; continue;
} }
@ -425,10 +438,8 @@ class MimeDir extends Parser
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
if (\is_null($property['value'])) { $property['value'] ??= '';
$property['value'] = ''; if (!isset($property['name']) || 0 === strlen($property['name'])) {
}
if (!$property['name']) {
if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
return false; return false;
} }
@ -671,7 +682,7 @@ class MimeDir extends Parser
// missing a whitespace. So if 'forgiving' is turned on, we will take // missing a whitespace. So if 'forgiving' is turned on, we will take
// those as well. // those as well.
if ($this->options & self::OPTION_FORGIVING) { if ($this->options & self::OPTION_FORGIVING) {
while ('=' === \substr($value, -1) && $this->lineBuffer) { while (str_ends_with($value, '=') && $this->lineBuffer) {
// Reading the line // Reading the line
$this->readLine(); $this->readLine();
// Grabbing the raw form // Grabbing the raw form

View file

@ -30,11 +30,6 @@ abstract class Parser
*/ */
public const OPTION_IGNORE_INVALID_LINES = 2; public const OPTION_IGNORE_INVALID_LINES = 2;
/**
* Bitmask of parser options.
*/
protected int $options;
/** /**
* Creates the parser. * Creates the parser.
* *
@ -42,12 +37,11 @@ abstract class Parser
* *
* @param int $options any parser options (OPTION constants) * @param int $options any parser options (OPTION constants)
*/ */
public function __construct($input = null, int $options = 0) public function __construct($input = null, protected int $options = 0)
{ {
if (!is_null($input)) { if (!is_null($input)) {
$this->setInput($input); $this->setInput($input);
} }
$this->options = $options;
} }
/** /**

View file

@ -28,17 +28,17 @@ class XML extends Parser
/** /**
* The input data. * The input data.
*/ */
protected ?array $input; protected ?array $input = null;
/** /**
* A pointer/reference to the input. * A pointer/reference to the input.
*/ */
private ?array $pointer; private ?array $pointer = null;
/** /**
* Document, root component. * Document, root component.
*/ */
protected ?Document $root; protected ?Document $root = null;
/** /**
* Creates the parser. * Creates the parser.
@ -112,7 +112,7 @@ class XML extends Parser
*/ */
protected function parseVCalendarComponents(Component $parentComponent): void protected function parseVCalendarComponents(Component $parentComponent): void
{ {
foreach ($this->pointer['value'] ?: [] as $children) { foreach ($this->pointer['value'] ?? [] as $children) {
switch (static::getTagName($children['name'])) { switch (static::getTagName($children['name'])) {
case 'properties': case 'properties':
$this->pointer = &$children['value']; $this->pointer = &$children['value'];
@ -145,8 +145,8 @@ class XML extends Parser
*/ */
protected function parseProperties(Component $parentComponent, string $propertyNamePrefix = ''): void protected function parseProperties(Component $parentComponent, string $propertyNamePrefix = ''): void
{ {
foreach ($this->pointer ?: [] as $xmlProperty) { foreach ($this->pointer ?? [] as $xmlProperty) {
list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); [$namespace, $tagName] = SabreXml\Service::parseClarkNotation($xmlProperty['name']);
$propertyName = $tagName; $propertyName = $tagName;
$propertyValue = []; $propertyValue = [];
@ -322,7 +322,7 @@ class XML extends Parser
*/ */
protected function parseComponent(Component $parentComponent): void protected function parseComponent(Component $parentComponent): void
{ {
$components = $this->pointer['value'] ?: []; $components = $this->pointer['value'] ?? [];
foreach ($components as $component) { foreach ($components as $component) {
$componentName = static::getTagName($component['name']); $componentName = static::getTagName($component['name']);

View file

@ -14,27 +14,8 @@ use Sabre\Xml;
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
abstract class Property extends Node abstract class Property extends Node implements \Stringable
{ {
/**
* The root document.
*/
public ?Component $root;
/**
* Property name.
*
* This will contain a string such as DTSTART, SUMMARY, FN.
*/
public ?string $name;
/**
* Property group.
*
* This is only used in vcards
*/
public ?string $group;
/** /**
* List of parameters. * List of parameters.
*/ */
@ -75,13 +56,13 @@ abstract class Property extends Node
* @param array $parameters List of parameters * @param array $parameters List of parameters
* @param string|null $group The vcard property group * @param string|null $group The vcard property group
*/ */
public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null, ?int $lineIndex = null, ?string $lineString = null) public function __construct(public ?Component $root, /**
* Property name.
*
* This will contain a string such as DTSTART, SUMMARY, FN.
*/
public ?string $name, $value = null, array $parameters = [], public ?string $group = null, ?int $lineIndex = null, ?string $lineString = null)
{ {
$this->name = $name;
$this->group = $group;
$this->root = $root;
foreach ($parameters as $k => $v) { foreach ($parameters as $k => $v) {
$this->add($k, $v); $this->add($k, $v);
} }
@ -123,7 +104,7 @@ abstract class Property extends Node
public function getValue() public function getValue()
{ {
if (is_array($this->value)) { if (is_array($this->value)) {
if (0 == count($this->value)) { if (0 === count($this->value)) {
return null; return null;
} elseif (1 === count($this->value)) { } elseif (1 === count($this->value)) {
return $this->value[0]; return $this->value[0];
@ -155,9 +136,9 @@ abstract class Property extends Node
return []; return [];
} elseif (is_array($this->value)) { } elseif (is_array($this->value)) {
return $this->value; return $this->value;
} else {
return [$this->value];
} }
return [$this->value];
} }
/** /**
@ -242,7 +223,7 @@ abstract class Property extends Node
); );
// remove single space after last CRLF // remove single space after last CRLF
return \substr($str, 0, -1); return \substr((string) $str, 0, -1);
} }
/** /**
@ -282,7 +263,7 @@ abstract class Property extends Node
if ('VALUE' === $parameter->name) { if ('VALUE' === $parameter->name) {
continue; continue;
} }
$parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); $parameters[strtolower((string) $parameter->name)] = $parameter->jsonSerialize();
} }
// In jCard, we need to encode the property-group as a separate 'group' // In jCard, we need to encode the property-group as a separate 'group'
// parameter. // parameter.
@ -292,7 +273,7 @@ abstract class Property extends Node
return array_merge( return array_merge(
[ [
strtolower($this->name), strtolower((string) $this->name),
(object) $parameters, (object) $parameters,
strtolower($this->getValueType()), strtolower($this->getValueType()),
], ],
@ -329,13 +310,13 @@ abstract class Property extends Node
$parameters[] = $parameter; $parameters[] = $parameter;
} }
$writer->startElement(strtolower($this->name)); $writer->startElement(strtolower((string) $this->name));
if (!empty($parameters)) { if ([] !== $parameters) {
$writer->startElement('parameters'); $writer->startElement('parameters');
foreach ($parameters as $parameter) { foreach ($parameters as $parameter) {
$writer->startElement(strtolower($parameter->name)); $writer->startElement(strtolower((string) $parameter->name));
$writer->write($parameter); $writer->write($parameter);
$writer->endElement(); $writer->endElement();
} }
@ -520,7 +501,7 @@ abstract class Property extends Node
} }
// Checking if the property name does not contain any invalid bytes. // Checking if the property name does not contain any invalid bytes.
if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { if (!preg_match('/^([A-Z0-9-]+)$/', (string) $this->name)) {
$warnings[] = [ $warnings[] = [
'level' => $options & self::REPAIR ? 1 : 3, 'level' => $options & self::REPAIR ? 1 : 3,
'message' => 'The property name: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', 'message' => 'The property name: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed',
@ -572,7 +553,7 @@ abstract class Property extends Node
} }
break; break;
} }
if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) { if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding, true)) {
$warnings[] = [ $warnings[] = [
'level' => 3, 'level' => 3,
'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.', 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.',

View file

@ -54,7 +54,7 @@ class Binary extends Property
*/ */
public function setRawMimeDirValue(string $val): void public function setRawMimeDirValue(string $val): void
{ {
$this->value = base64_decode($val); $this->value = base64_decode($val, true);
} }
/** /**
@ -62,7 +62,7 @@ class Binary extends Property
*/ */
public function getRawMimeDirValue(): string public function getRawMimeDirValue(): string
{ {
return base64_encode($this->value); return base64_encode((string) $this->value);
} }
/** /**
@ -83,7 +83,7 @@ class Binary extends Property
*/ */
public function getJsonValue(): array public function getJsonValue(): array
{ {
return [base64_encode($this->getValue())]; return [base64_encode((string) $this->getValue())];
} }
/** /**
@ -93,7 +93,7 @@ class Binary extends Property
*/ */
public function setJsonValue(array $value): void public function setJsonValue(array $value): void
{ {
$value = array_map('base64_decode', $value); $value = array_map(base64_decode(...), $value);
parent::setJsonValue($value); parent::setJsonValue($value);
} }
} }

View file

@ -56,9 +56,7 @@ class Boolean extends Property
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($value) { fn ($value) => 'true' === $value,
return 'true' === $value;
},
$value $value
); );
parent::setXmlValue($value); parent::setXmlValue($value);

View file

@ -67,7 +67,7 @@ class FloatValue extends Property
*/ */
public function getJsonValue(): array public function getJsonValue(): array
{ {
$val = array_map('floatval', $this->getParts()); $val = array_map(floatval(...), $this->getParts());
// Special-casing the GEO property. // Special-casing the GEO property.
// //
@ -86,7 +86,7 @@ class FloatValue extends Property
*/ */
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map('floatval', $value); $value = array_map(floatval(...), $value);
parent::setXmlValue($value); parent::setXmlValue($value);
} }
@ -101,7 +101,7 @@ class FloatValue extends Property
// See: // See:
// http://tools.ietf.org/html/rfc6321#section-3.4.1.2 // http://tools.ietf.org/html/rfc6321#section-3.4.1.2
if ('GEO' === $this->name) { if ('GEO' === $this->name) {
$value = array_map('floatval', $this->getParts()); $value = array_map(floatval(...), $this->getParts());
$writer->writeElement('latitude', $value[0]); $writer->writeElement('latitude', $value[0]);
$writer->writeElement('longitude', $value[1]); $writer->writeElement('longitude', $value[1]);

View file

@ -43,10 +43,10 @@ class CalAddress extends Text
public function getNormalizedValue(): string public function getNormalizedValue(): string
{ {
$input = $this->getValue(); $input = $this->getValue();
if (!strpos($input, ':')) { if (!strpos((string) $input, ':')) {
return $input; return $input;
} }
list($schema, $everythingElse) = explode(':', $input, 2); [$schema, $everythingElse] = explode(':', (string) $input, 2);
$schema = strtolower($schema); $schema = strtolower($schema);
if ('mailto' === $schema) { if ('mailto' === $schema) {
$everythingElse = strtolower($everythingElse); $everythingElse = strtolower($everythingElse);

View file

@ -111,7 +111,7 @@ class DateTime extends Property
!$this->hasTime() !$this->hasTime()
|| ( || (
!isset($this['TZID']) !isset($this['TZID'])
&& false === strpos($this->getValue(), 'Z') && !str_contains((string) $this->getValue(), 'Z')
); );
} }
@ -205,7 +205,7 @@ class DateTime extends Property
} }
if (is_null($tz)) { if (is_null($tz)) {
$tz = $d->getTimeZone(); $tz = $d->getTimeZone();
$isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']); $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00'], true);
if (!$isUtc) { if (!$isUtc) {
$this->offsetSet('TZID', $tz->getName()); $this->offsetSet('TZID', $tz->getName());
} }
@ -257,15 +257,15 @@ class DateTime extends Property
$isFloating = $this->isFloating(); $isFloating = $this->isFloating();
$tz = $dts[0]->getTimeZone(); $tz = $dts[0]->getTimeZone();
$isUtc = !$isFloating && in_array($tz->getName(), ['UTC', 'GMT', 'Z']); $isUtc = !$isFloating && in_array($tz->getName(), ['UTC', 'GMT', 'Z'], true);
return array_map( return array_map(
function (\DateTimeInterface $dt) use ($hasTime, $isUtc) { function (\DateTimeInterface $dt) use ($hasTime, $isUtc) {
if ($hasTime) { if ($hasTime) {
return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : '');
} else {
return $dt->format('Y-m-d');
} }
return $dt->format('Y-m-d');
}, },
$dts $dts
); );
@ -286,9 +286,7 @@ class DateTime extends Property
// those. // those.
$this->setValue( $this->setValue(
array_map( array_map(
function ($item) { fn ($item) => strtr($item, [':' => '', '-' => '']),
return strtr($item, [':' => '', '-' => '']);
},
$value $value
) )
); );
@ -306,7 +304,7 @@ class DateTime extends Property
public function offsetSet($offset, $value): void public function offsetSet($offset, $value): void
{ {
parent::offsetSet($offset, $value); parent::offsetSet($offset, $value);
if ('VALUE' !== strtoupper($offset)) { if ('VALUE' !== strtoupper((string) $offset)) {
return; return;
} }
@ -347,7 +345,7 @@ class DateTime extends Property
DateTimeParser::parseDateTime($value); DateTimeParser::parseDateTime($value);
break; break;
} }
} catch (InvalidDataException $e) { } catch (InvalidDataException) {
$messages[] = [ $messages[] = [
'level' => 3, 'level' => 3,
'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType,

View file

@ -64,14 +64,17 @@ class Period extends Property
public function setJsonValue(array $value): void public function setJsonValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($item) { fn ($item) => strtr(implode('/', $item), [':' => '', '-' => '']),
return strtr(implode('/', $item), [':' => '', '-' => '']);
},
$value $value
); );
parent::setJsonValue($value); parent::setJsonValue($value);
} }
public function appendUtc(string $strDate)
{
return !str_ends_with($strDate, 'Z') ? '' : 'Z';
}
/** /**
* Returns the value, in the format it should be encoded for json. * Returns the value, in the format it should be encoded for json.
* *
@ -83,21 +86,21 @@ class Period extends Property
{ {
$return = []; $return = [];
foreach ($this->getParts() as $item) { foreach ($this->getParts() as $item) {
list($start, $end) = explode('/', $item, 2); [$start, $end] = explode('/', (string) $item, 2);
$start = DateTimeParser::parseDateTime($start); $startDt = DateTimeParser::parseDateTime($start)->format('Y-m-d\\TH:i:s').$this->appendUtc($start);
// This is a duration value. // This is a duration value.
if ('P' === $end[0]) { if ('P' === $end[0]) {
$return[] = [ $return[] = [
$start->format('Y-m-d\\TH:i:s'), $startDt,
$end, $end,
]; ];
} else { } else {
$end = DateTimeParser::parseDateTime($end); $endDt = DateTimeParser::parseDateTime($end)->format('Y-m-d\\TH:i:s').$this->appendUtc($end);
$return[] = [ $return[] = [
$start->format('Y-m-d\\TH:i:s'), $startDt,
$end->format('Y-m-d\\TH:i:s'), $endDt,
]; ];
} }
} }

View file

@ -29,14 +29,14 @@ class Recur extends Property
/** /**
* Reference to the parent object, if this is not the top object. * Reference to the parent object, if this is not the top object.
*/ */
public ?Node $parent; public ?Node $parent = null;
/** /**
* Updates the current value. * Updates the current value.
* *
* This may be either a single, or multiple strings in an array. * This may be either a single, or multiple strings in an array.
* *
* @param string|array $value * @param string|array|object $value
*/ */
public function setValue($value): void public function setValue($value): void
{ {
@ -52,17 +52,17 @@ class Recur extends Property
$v = strtoupper($v); $v = strtoupper($v);
// The value had multiple sub-values // The value had multiple sub-values
if (false !== strpos($v, ',')) { if (str_contains($v, ',')) {
$v = explode(',', $v); $v = explode(',', $v);
} }
if (0 === strcmp($k, 'until')) { if (0 === strcmp((string) $k, 'until')) {
$v = strtr($v, [':' => '', '-' => '']); $v = strtr($v, [':' => '', '-' => '']);
} }
} elseif (is_array($v)) { } elseif (is_array($v)) {
$v = array_map('strtoupper', $v); $v = array_map(strtoupper(...), $v);
} }
$newVal[strtoupper($k)] = $v; $newVal[strtoupper((string) $k)] = $v;
} }
$this->value = $newVal; $this->value = $newVal;
} elseif (is_string($value)) { } elseif (is_string($value)) {
@ -151,13 +151,13 @@ class Recur extends Property
{ {
$values = []; $values = [];
foreach ($this->getParts() as $k => $v) { foreach ($this->getParts() as $k => $v) {
if (0 === strcmp($k, 'UNTIL')) { if (0 === strcmp((string) $k, 'UNTIL')) {
$date = new DateTime($this->root, null, $v); $date = new DateTime($this->root, null, $v);
$values[strtolower($k)] = $date->getJsonValue()[0]; $values[strtolower((string) $k)] = $date->getJsonValue()[0];
} elseif (0 === strcmp($k, 'COUNT')) { } elseif (0 === strcmp((string) $k, 'COUNT')) {
$values[strtolower($k)] = intval($v); $values[strtolower((string) $k)] = intval($v);
} else { } else {
$values[strtolower($k)] = $v; $values[strtolower((string) $k)] = $v;
} }
} }
@ -186,7 +186,7 @@ class Recur extends Property
$newValue = []; $newValue = [];
foreach (explode(';', $value) as $part) { foreach (explode(';', $value) as $part) {
// Skipping empty parts. // Skipping empty parts.
if (empty($part)) { if ('' === $part) {
continue; continue;
} }
@ -196,10 +196,10 @@ class Recur extends Property
throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part); throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part);
} }
list($partName, $partValue) = $parts; [$partName, $partValue] = $parts;
// The value itself had multiple values.. // The value itself had multiple values..
if (false !== strpos($partValue, ',')) { if (str_contains($partValue, ',')) {
$partValue = explode(',', $partValue); $partValue = explode(',', $partValue);
} }
$newValue[$partName] = $partValue; $newValue[$partName] = $partValue;
@ -243,7 +243,7 @@ class Recur extends Property
if ($repair) { if ($repair) {
unset($values[$key]); unset($values[$key]);
} }
} elseif ('BYMONTH' == $key) { } elseif ('BYMONTH' === $key) {
$byMonth = (array) $value; $byMonth = (array) $value;
foreach ($byMonth as $i => $v) { foreach ($byMonth as $i => $v) {
if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) { if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) {
@ -262,13 +262,13 @@ class Recur extends Property
} }
} }
// if there is no valid entry left, remove the whole value // if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) { if (is_array($value) && ([] === $values[$key])) {
unset($values[$key]); unset($values[$key]);
} }
} elseif ('BYWEEKNO' == $key) { } elseif ('BYWEEKNO' === $key) {
$byWeekNo = (array) $value; $byWeekNo = (array) $value;
foreach ($byWeekNo as $i => $v) { foreach ($byWeekNo as $i => $v) {
if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) { if (!is_numeric($v) || (int) $v < -53 || 0 === (int) $v || (int) $v > 53) {
$warnings[] = [ $warnings[] = [
'level' => $repair ? 1 : 3, 'level' => $repair ? 1 : 3,
'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!',
@ -284,13 +284,13 @@ class Recur extends Property
} }
} }
// if there is no valid entry left, remove the whole value // if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) { if (is_array($value) && ([] === $values[$key])) {
unset($values[$key]); unset($values[$key]);
} }
} elseif ('BYYEARDAY' == $key) { } elseif ('BYYEARDAY' === $key) {
$byYearDay = (array) $value; $byYearDay = (array) $value;
foreach ($byYearDay as $i => $v) { foreach ($byYearDay as $i => $v) {
if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) { if (!is_numeric($v) || (int) $v < -366 || 0 === (int) $v || (int) $v > 366) {
$warnings[] = [ $warnings[] = [
'level' => $repair ? 1 : 3, 'level' => $repair ? 1 : 3,
'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!',
@ -306,7 +306,7 @@ class Recur extends Property
} }
} }
// if there is no valid entry left, remove the whole value // if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) { if (is_array($value) && ([] === $values[$key])) {
unset($values[$key]); unset($values[$key]);
} }
} }

View file

@ -62,7 +62,7 @@ class IntegerValue extends Property
*/ */
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map('intval', $value); $value = array_map(intval(...), $value);
parent::setXmlValue($value); parent::setXmlValue($value);
} }
} }

View file

@ -71,7 +71,7 @@ class Text extends Property
// 2. structured value properties // 2. structured value properties
// //
// The former is always separated by a comma, the latter by semicolon. // The former is always separated by a comma, the latter by semicolon.
if (in_array($name, $this->structuredValues)) { if (in_array($name, $this->structuredValues, true)) {
$this->delimiter = ';'; $this->delimiter = ';';
} }
@ -155,7 +155,7 @@ class Text extends Property
// Structured text values should always be returned as a single // Structured text values should always be returned as a single
// array-item. Multi-value text should be returned as multiple items in // array-item. Multi-value text should be returned as multiple items in
// the top-array. // the top-array.
if (in_array($this->name, $this->structuredValues)) { if (in_array($this->name, $this->structuredValues, true)) {
return [$this->getParts()]; return [$this->getParts()];
} }
@ -213,7 +213,7 @@ class Text extends Property
// If the resulting value contains a \n, we must encode it as // If the resulting value contains a \n, we must encode it as
// quoted-printable. // quoted-printable.
if (false !== \strpos($val, "\n")) { if (str_contains((string) $val, "\n")) {
$str .= ';ENCODING=QUOTED-PRINTABLE:'; $str .= ';ENCODING=QUOTED-PRINTABLE:';
$lastLine = $str; $lastLine = $str;
$out = ''; $out = '';
@ -222,13 +222,13 @@ class Text extends Property
// encode newlines for us. Specifically, the \r\n sequence must in // encode newlines for us. Specifically, the \r\n sequence must in
// vcards be encoded as =0D=OA and we must insert soft-newlines // vcards be encoded as =0D=OA and we must insert soft-newlines
// every 75 bytes. // every 75 bytes.
for ($ii = 0; $ii < \strlen($val); ++$ii) { for ($ii = 0; $ii < \strlen((string) $val); ++$ii) {
$ord = \ord($val[$ii]); $ord = \ord($val[$ii]);
// These characters are encoded as themselves. // These characters are encoded as themselves.
if ($ord >= 32 && $ord <= 126) { if ($ord >= 32 && $ord <= 126) {
$lastLine .= $val[$ii]; $lastLine .= $val[$ii];
} else { } else {
$lastLine .= '='.\strtoupper(\bin2hex($val[$ii])); $lastLine .= '='.\strtoupper(\bin2hex((string) $val[$ii]));
} }
if (\strlen($lastLine) >= 75) { if (\strlen($lastLine) >= 75) {
// Soft line break // Soft line break
@ -241,22 +241,21 @@ class Text extends Property
} }
return $out; return $out;
} else { }
$str .= ':'.$val; $str .= ':'.$val;
$str = \preg_replace( $str = \preg_replace(
'/( '/(
(?:^.)? # 1 additional byte in first line because of missing single space (see next line) (?:^.)? # 1 additional byte in first line because of missing single space (see next line)
.{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
(?![\x80-\xbf]) # prevent splitting multibyte characters (?![\x80-\xbf]) # prevent splitting multibyte characters
)/x', )/x',
"$1\r\n ", "$1\r\n ",
$str $str
); );
// remove single space after last CRLF // remove single space after last CRLF
return \substr($str, 0, -1); return \substr((string) $str, 0, -1);
}
} }
/** /**
@ -271,7 +270,7 @@ class Text extends Property
foreach ($items as $i => $item) { foreach ($items as $i => $item) {
$writer->writeElement( $writer->writeElement(
$item, $item,
!empty($values[$i]) ? $values[$i] : null ('' !== $values[$i] && [] !== $values[$i]) ? $values[$i] : null
); );
} }
}; };

View file

@ -104,7 +104,7 @@ class Time extends Text
$timeStr .= 'Z'; $timeStr .= 'Z';
} else { } else {
$timeStr .= $timeStr .=
preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', (string) $parts['timezone']);
} }
} }
@ -118,9 +118,7 @@ class Time extends Text
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($value) { fn ($value) => str_replace(':', '', $value),
return str_replace(':', '', $value);
},
$value $value
); );
parent::setXmlValue($value); parent::setXmlValue($value);

View file

@ -38,7 +38,7 @@ class Uri extends Text
public function parameters(): array public function parameters(): array
{ {
$parameters = parent::parameters(); $parameters = parent::parameters();
if (!isset($parameters['VALUE']) && in_array($this->name, ['URL', 'PHOTO'])) { if (!isset($parameters['VALUE']) && in_array($this->name, ['URL', 'PHOTO'], true)) {
// If we are encoding a URI value, and this URI value has no // If we are encoding a URI value, and this URI value has no
// VALUE=URI parameter, we add it anyway. // VALUE=URI parameter, we add it anyway.
// //
@ -74,14 +74,10 @@ class Uri extends Text
$matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$newVal = ''; $newVal = '';
foreach ($matches as $match) { foreach ($matches as $match) {
switch ($match) { match ($match) {
case '\:': '\:' => $newVal .= ':',
$newVal .= ':'; default => $newVal .= $match,
break; };
default:
$newVal .= $match;
break;
}
} }
$this->value = $newVal; $this->value = $newVal;
} else { } else {

View file

@ -42,9 +42,7 @@ class UtcOffset extends Text
public function setJsonValue(array $value): void public function setJsonValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($value) { fn ($value) => str_replace(':', '', $value),
return str_replace(':', '', $value);
},
$value $value
); );
parent::setJsonValue($value); parent::setJsonValue($value);
@ -58,10 +56,8 @@ class UtcOffset extends Text
public function getJsonValue(): array public function getJsonValue(): array
{ {
return array_map( return array_map(
function ($value) { fn ($value) => substr((string) $value, 0, -2).':'.
return substr($value, 0, -2).':'. substr((string) $value, -2),
substr($value, -2);
},
parent::getJsonValue() parent::getJsonValue()
); );
} }

View file

@ -77,7 +77,7 @@ class DateAndOrTime extends Property
public function setDateTime(\DateTimeInterface $dt): void public function setDateTime(\DateTimeInterface $dt): void
{ {
$tz = $dt->getTimeZone(); $tz = $dt->getTimeZone();
$isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']); $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z'], true);
if ($isUtc) { if ($isUtc) {
$value = $dt->format('Ymd\\THis\\Z'); $value = $dt->format('Ymd\\THis\\Z');
@ -232,14 +232,10 @@ class DateAndOrTime extends Property
$value = ''; $value = '';
// $d = defined // $d = defined
$d = function ($part) use ($parts): bool { $d = (fn ($part): bool => !is_null($parts[$part]));
return !is_null($parts[$part]);
};
// $r = read // $r = read
$r = function ($part) use ($parts) { $r = (fn ($part) => $parts[$part]);
return $parts[$part];
};
// From the Relax NG Schema. // From the Relax NG Schema.
// //
@ -341,7 +337,7 @@ class DateAndOrTime extends Property
try { try {
DateTimeParser::parseVCardDateTime($value); DateTimeParser::parseVCardDateTime($value);
} catch (InvalidDataException $e) { } catch (InvalidDataException) {
$messages[] = [ $messages[] = [
'level' => 3, 'level' => 3,
'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property', 'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property',

View file

@ -91,9 +91,7 @@ class EventIterator implements \Iterator
*/ */
public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZone = null) public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZone = null)
{ {
if (is_null($timeZone)) { $timeZone ??= new \DateTimeZone('UTC');
$timeZone = new \DateTimeZone('UTC');
}
$this->timeZone = $timeZone; $this->timeZone = $timeZone;
if (is_array($input)) { if (is_array($input)) {
@ -164,8 +162,12 @@ class EventIterator implements \Iterator
} }
if (isset($this->masterEvent->RDATE)) { if (isset($this->masterEvent->RDATE)) {
$rdateValues = [];
foreach ($this->masterEvent->RDATE as $rdate) {
$rdateValues = array_merge($rdateValues, $rdate->getParts());
}
$this->recurIterator = new RDateIterator( $this->recurIterator = new RDateIterator(
$this->masterEvent->RDATE->getParts(), $rdateValues,
$this->startDate $this->startDate
); );
} elseif (isset($this->masterEvent->RRULE)) { } elseif (isset($this->masterEvent->RRULE)) {
@ -228,11 +230,10 @@ class EventIterator implements \Iterator
} }
if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) { if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) {
return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone); return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone);
} else {
$end = clone $this->currentDate;
return $end->modify('+'.$this->eventDuration.' seconds');
} }
$end = clone $this->currentDate;
return $end->modify('+'.$this->eventDuration.' seconds');
} }
/** /**

View file

@ -26,9 +26,13 @@ class RDateIterator implements \Iterator
* *
* @param string|array $rrule * @param string|array $rrule
*/ */
public function __construct($rrule, \DateTimeInterface $start) public function __construct($rrule, /**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate)
{ {
$this->startDate = $start;
$this->parseRDate($rrule); $this->parseRDate($rrule);
$this->currentDate = clone $this->startDate; $this->currentDate = clone $this->startDate;
} }
@ -117,13 +121,6 @@ class RDateIterator implements \Iterator
} }
} }
/**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate;
/** /**
* The date of the current iteration. You can get this by calling * The date of the current iteration. You can get this by calling
* ->current(). * ->current().

View file

@ -37,9 +37,13 @@ class RRuleIterator implements \Iterator
* *
* @throws InvalidDataException * @throws InvalidDataException
*/ */
public function __construct($rrule, \DateTimeInterface $start) public function __construct($rrule, /**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate)
{ {
$this->startDate = $start;
$this->parseRRule($rrule); $this->parseRRule($rrule);
$this->currentDate = clone $this->startDate; $this->currentDate = clone $this->startDate;
} }
@ -146,13 +150,6 @@ class RRuleIterator implements \Iterator
} }
} }
/**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate;
/** /**
* The date of the current iteration. You can get this by calling * The date of the current iteration. You can get this by calling
* ->current(). * ->current().
@ -357,23 +354,23 @@ class RRuleIterator implements \Iterator
} }
$recurrenceHours = []; $recurrenceHours = [];
if (!empty($this->byHour)) { if ($this->byHour) {
$recurrenceHours = $this->getHours(); $recurrenceHours = $this->getHours();
} }
$recurrenceDays = []; $recurrenceDays = [];
if (!empty($this->byDay)) { if ($this->byDay) {
$recurrenceDays = $this->getDays(); $recurrenceDays = $this->getDays();
} }
$recurrenceMonths = []; $recurrenceMonths = [];
if (!empty($this->byMonth)) { if ($this->byMonth) {
$recurrenceMonths = $this->getMonths(); $recurrenceMonths = $this->getMonths();
} }
do { do {
if ($this->byHour) { if ($this->byHour) {
if ('23' == $this->currentDate->format('G')) { if ('23' === $this->currentDate->format('G')) {
// to obey the interval rule // to obey the interval rule
$this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days');
} }
@ -398,9 +395,9 @@ class RRuleIterator implements \Iterator
return; return;
} }
} while ( } while (
($this->byDay && !in_array($currentDay, $recurrenceDays)) ($this->byDay && !in_array((int) $currentDay, $recurrenceDays, true))
|| ($this->byHour && !in_array($currentHour, $recurrenceHours)) || ($this->byHour && !in_array($currentHour, $recurrenceHours, true))
|| ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)) || ($this->byMonth && !in_array($currentMonth, $recurrenceMonths, true))
); );
} }
@ -442,18 +439,18 @@ class RRuleIterator implements \Iterator
$currentHour = (int) $this->currentDate->format('G'); $currentHour = (int) $this->currentDate->format('G');
// We need to roll over to the next week // We need to roll over to the next week
if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) { if ($currentDay === $firstDay && (!$this->byHour || 0 === $currentHour)) {
$this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks');
// We need to go to the first day of this week, but only if we // We need to go to the first day of this week, but only if we
// are not already on this first day of this week. // are not already on this first day of this week.
if ($this->currentDate->format('w') != $firstDay) { if ((int) $this->currentDate->format('w') !== $firstDay) {
$this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]); $this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]);
} }
} }
// We have a match // We have a match
} while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); } while (($this->byDay && !in_array($currentDay, $recurrenceDays, true)) || ($this->byHour && !in_array((string) $currentHour, $recurrenceHours, true)));
} }
/** /**
@ -476,14 +473,14 @@ class RRuleIterator implements \Iterator
++$increase; ++$increase;
$tempDate = clone $this->currentDate; $tempDate = clone $this->currentDate;
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime());
} while ($tempDate->format('j') != $currentDayOfMonth); } while ($tempDate->format('j') !== $currentDayOfMonth);
$this->currentDate = $tempDate; $this->currentDate = $tempDate;
} }
return; return;
} }
$occurrence = -1; $selectedOccurrence = -1;
while (true) { while (true) {
$occurrences = $this->getMonthlyOccurrences(); $occurrences = $this->getMonthlyOccurrences();
@ -491,6 +488,7 @@ class RRuleIterator implements \Iterator
// The first occurrence that's higher than the current // The first occurrence that's higher than the current
// day of the month wins. // day of the month wins.
if ($occurrence > $currentDayOfMonth) { if ($occurrence > $currentDayOfMonth) {
$selectedOccurrence = $occurrence;
break 2; break 2;
} }
} }
@ -533,7 +531,7 @@ class RRuleIterator implements \Iterator
$this->currentDate = $this->currentDate->setDate( $this->currentDate = $this->currentDate->setDate(
(int) $this->currentDate->format('Y'), (int) $this->currentDate->format('Y'),
(int) $this->currentDate->format('n'), (int) $this->currentDate->format('n'),
(int) $occurrence (int) $selectedOccurrence
)->modify($this->startTime()); )->modify($this->startTime());
} }
@ -547,9 +545,9 @@ class RRuleIterator implements \Iterator
$currentDayOfMonth = $this->currentDate->format('j'); $currentDayOfMonth = $this->currentDate->format('j');
// No sub-rules, so we just advance by year // No sub-rules, so we just advance by year
if (empty($this->byMonth)) { if (!$this->byMonth) {
// Unless it was a leap day! // Unless it was a leap day!
if (2 == $currentMonth && 29 == $currentDayOfMonth) { if (2 === (int) $currentMonth && 29 === (int) $currentDayOfMonth) {
$counter = 0; $counter = 0;
do { do {
++$counter; ++$counter;
@ -563,7 +561,7 @@ class RRuleIterator implements \Iterator
// functions instead. // functions instead.
$nextDate = clone $this->currentDate; $nextDate = clone $this->currentDate;
$nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years'); $nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years');
} while (2 != $nextDate->format('n')); } while (2 !== (int) $nextDate->format('n'));
$this->currentDate = $nextDate; $this->currentDate = $nextDate;
@ -634,7 +632,7 @@ class RRuleIterator implements \Iterator
$date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D')); $date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D'));
} }
if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) { if ($date > $this->currentDate && in_array((int) $date->format('N'), $dayOffsets, true)) {
$checkDates[] = $date; $checkDates[] = $date;
} }
} }
@ -665,7 +663,7 @@ class RRuleIterator implements \Iterator
// If we got a byDay or getMonthDay filter, we must first expand // If we got a byDay or getMonthDay filter, we must first expand
// further. // further.
if ($this->byDay || $this->byMonthDay) { if ($this->byDay || $this->byMonthDay) {
$occurrence = -1; $selectedOccurrence = -1;
while (true) { while (true) {
$occurrences = $this->getMonthlyOccurrences(); $occurrences = $this->getMonthlyOccurrences();
@ -677,7 +675,8 @@ class RRuleIterator implements \Iterator
if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
// only consider byMonth matches, // only consider byMonth matches,
// otherwise, we don't follow RRule correctly // otherwise, we don't follow RRule correctly
if (in_array($currentMonth, $this->byMonth)) { if (in_array((string) $currentMonth, $this->byMonth, true)) {
$selectedOccurrence = $occurrence;
break 2; break 2;
} }
} }
@ -693,12 +692,12 @@ class RRuleIterator implements \Iterator
$currentYear += $this->interval; $currentYear += $this->interval;
$currentMonth = 1; $currentMonth = 1;
} }
} while (!in_array($currentMonth, $this->byMonth)); } while (!in_array((string) $currentMonth, $this->byMonth, true));
$this->currentDate = $this->currentDate->setDate( $this->currentDate = $this->currentDate->setDate(
(int) $currentYear, (int) $currentYear,
(int) $currentMonth, $currentMonth,
(int) $currentDayOfMonth $currentDayOfMonth
); );
// To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply
@ -714,28 +713,25 @@ class RRuleIterator implements \Iterator
$this->currentDate = $this->currentDate->setDate( $this->currentDate = $this->currentDate->setDate(
(int) $currentYear, (int) $currentYear,
(int) $currentMonth, (int) $currentMonth,
(int) $occurrence (int) $selectedOccurrence
)->modify($this->startTime());
return;
} else {
// These are the 'byMonth' rules, if there are no byDay or
// byMonthDay sub-rules.
do {
++$currentMonth;
if ($currentMonth > 12) {
$currentYear += $this->interval;
$currentMonth = 1;
}
} while (!in_array($currentMonth, $this->byMonth));
$this->currentDate = $this->currentDate->setDate(
(int) $currentYear,
(int) $currentMonth,
(int) $currentDayOfMonth
)->modify($this->startTime()); )->modify($this->startTime());
return; return;
} }
// These are the 'byMonth' rules, if there are no byDay or
// byMonthDay sub-rules.
do {
++$currentMonth;
if ($currentMonth > 12) {
$currentYear += $this->interval;
$currentMonth = 1;
}
} while (!in_array((string) $currentMonth, $this->byMonth, true));
$this->currentDate = $this->currentDate->setDate(
(int) $currentYear,
$currentMonth,
(int) $currentDayOfMonth
)->modify($this->startTime());
} }
/* }}} */ /* }}} */
@ -755,13 +751,14 @@ class RRuleIterator implements \Iterator
} }
foreach ($rrule as $key => $value) { foreach ($rrule as $key => $value) {
$key = strtoupper($key); $key = strtoupper((string) $key);
switch ($key) { switch ($key) {
case 'FREQ': case 'FREQ':
$value = strtolower($value); $value = strtolower((string) $value);
if (!in_array( if (!in_array(
$value, $value,
['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'] ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'],
true
)) { )) {
throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value)); throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value));
} }
@ -809,7 +806,7 @@ class RRuleIterator implements \Iterator
case 'BYDAY': case 'BYDAY':
$value = (array) $value; $value = (array) $value;
foreach ($value as $part) { foreach ($value as $part) {
if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $#xi', (string) $part)) {
throw new InvalidDataException('Invalid part in BYDAY clause: '.$part); throw new InvalidDataException('Invalid part in BYDAY clause: '.$part);
} }
} }
@ -823,7 +820,7 @@ class RRuleIterator implements \Iterator
case 'BYYEARDAY': case 'BYYEARDAY':
$this->byYearDay = (array) $value; $this->byYearDay = (array) $value;
foreach ($this->byYearDay as $byYearDay) { foreach ($this->byYearDay as $byYearDay) {
if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 == (int) $byYearDay || (int) $byYearDay > 366) { if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 === (int) $byYearDay || (int) $byYearDay > 366) {
throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!'); throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!');
} }
} }
@ -832,7 +829,7 @@ class RRuleIterator implements \Iterator
case 'BYWEEKNO': case 'BYWEEKNO':
$this->byWeekNo = (array) $value; $this->byWeekNo = (array) $value;
foreach ($this->byWeekNo as $byWeekNo) { foreach ($this->byWeekNo as $byWeekNo) {
if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 == (int) $byWeekNo || (int) $byWeekNo > 53) { if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 === (int) $byWeekNo || (int) $byWeekNo > 53) {
throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!'); throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!');
} }
} }
@ -852,7 +849,7 @@ class RRuleIterator implements \Iterator
break; break;
case 'WKST': case 'WKST':
$this->weekStart = strtoupper($value); $this->weekStart = strtoupper((string) $value);
break; break;
default: default:
@ -892,7 +889,7 @@ class RRuleIterator implements \Iterator
// that point and add it to the results. // that point and add it to the results.
if ($this->byDay) { if ($this->byDay) {
foreach ($this->byDay as $day) { foreach ($this->byDay as $day) {
$dayName = $this->dayNames[$this->dayMap[substr($day, -2)]]; $dayName = $this->dayNames[$this->dayMap[substr((string) $day, -2)]];
// Dayname will be something like 'wednesday'. Now we need to find // Dayname will be something like 'wednesday'. Now we need to find
// all wednesdays in this month. // all wednesdays in this month.
@ -913,8 +910,8 @@ class RRuleIterator implements \Iterator
// So now we have 'all wednesdays' for month. It is however // So now we have 'all wednesdays' for month. It is however
// possible that the user only really wanted the 1st, 2nd or last // possible that the user only really wanted the 1st, 2nd or last
// wednesday. // wednesday.
if (strlen($day) > 2) { if (strlen((string) $day) > 2) {
$offset = (int) substr($day, 0, -2); $offset = (int) substr((string) $day, 0, -2);
if ($offset > 0) { if ($offset > 0) {
// It is possible that the day does not exist, such as a // It is possible that the day does not exist, such as a
@ -949,7 +946,7 @@ class RRuleIterator implements \Iterator
$byMonthDayResults[] = $monthDay; $byMonthDayResults[] = $monthDay;
} else { } else {
// Negative values // Negative values
$byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; $byMonthDayResults[] = (int) $startDate->format('t') + 1 + $monthDay;
} }
} }
} }
@ -1018,7 +1015,7 @@ class RRuleIterator implements \Iterator
// The day may be preceded with a positive (+n) or // The day may be preceded with a positive (+n) or
// negative (-n) integer. However, this does not make // negative (-n) integer. However, this does not make
// sense in 'weekly' so we ignore it here. // sense in 'weekly' so we ignore it here.
$recurrenceDays[] = $this->dayMap[substr($byDay, -2)]; $recurrenceDays[] = $this->dayMap[substr((string) $byDay, -2)];
} }
return $recurrenceDays; return $recurrenceDays;

View file

@ -96,8 +96,8 @@ class ICalendar implements SplitterInterface
} }
return $object; return $object;
} else {
return null;
} }
return null;
} }
} }

View file

@ -22,13 +22,6 @@ use Sabre\VObject\Parser\MimeDir;
*/ */
class VCard implements SplitterInterface class VCard implements SplitterInterface
{ {
/**
* File handle.
*
* @var resource
*/
protected $input;
/** /**
* Persistent parser. * Persistent parser.
*/ */
@ -42,10 +35,12 @@ class VCard implements SplitterInterface
* @param resource $input * @param resource $input
* @param int $options parser options, see the OPTIONS constants * @param int $options parser options, see the OPTIONS constants
*/ */
public function __construct($input, int $options = 0) public function __construct(/**
* File handle.
*/
protected $input, int $options = 0)
{ {
$this->input = $input; $this->parser = new MimeDir($this->input, $options);
$this->parser = new MimeDir($input, $options);
} }
/** /**
@ -64,7 +59,7 @@ class VCard implements SplitterInterface
if (!$object instanceof Component\VCard) { if (!$object instanceof Component\VCard) {
throw new VObject\ParseException('The supplied input contained non-VCARD data.'); throw new VObject\ParseException('The supplied input contained non-VCARD data.');
} }
} catch (VObject\EofException $e) { } catch (VObject\EofException) {
return null; return null;
} }

View file

@ -41,9 +41,7 @@ class TimeZoneUtil
private static function getInstance(): self private static function getInstance(): self
{ {
if (null === self::$instance) { self::$instance ??= new self();
self::$instance = new self();
}
return self::$instance; return self::$instance;
} }

View file

@ -24,6 +24,24 @@ class FindFromOffset implements TimezoneFinder
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
if (0 === strncasecmp($tzid, 'UTC', 3)) {
$offset = substr($tzid, 3);
if (6 === strlen($offset) && ':' === $offset[3]) {
$offset = substr($offset, 0, 3).substr($offset, 4);
}
if (5 !== strlen($offset)
|| ('+' !== $offset[0] && '-' !== $offset[0])
|| 4 !== strspn(substr($offset, 1), '0123456789')
|| (int) substr($offset, 1, 2) > 23
|| (int) substr($offset, 3, 2) > 59
) {
return null;
}
return new \DateTimeZone($offset);
}
return null; return null;
} }
} }

View file

@ -40,13 +40,13 @@ class FindFromTimezoneIdentifier implements TimezoneFinder
try { try {
if ( if (
in_array($tzid, $tzIdentifiers) in_array($tzid, $tzIdentifiers, true)
|| preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches) || preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)
|| in_array($tzid, $this->getIdentifiersBC()) || in_array($tzid, $this->getIdentifiersBC(), true)
) { ) {
return new \DateTimeZone($tzid); return new \DateTimeZone($tzid);
} }
} catch (\Exception $e) { } catch (\Exception) {
} }
return null; return null;

View file

@ -18,11 +18,18 @@ class FindFromTimezoneMap implements TimezoneFinder
'/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/', '/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/',
]; ];
/**
* @throws void
*/
public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone
{ {
// Next, we check if the tzid is somewhere in our tzid map. // Next, we check if the tzid is somewhere in our tzid map.
if ($this->hasTzInMap($tzid)) { if ($this->hasTzInMap($tzid)) {
return new \DateTimeZone($this->getTzFromMap($tzid)); try {
return new \DateTimeZone($this->getTzFromMap($tzid));
} catch (\Exception) {
return null;
}
} }
// Some Microsoft products prefix the offset first, so let's strip that off // Some Microsoft products prefix the offset first, so let's strip that off
@ -34,7 +41,11 @@ class FindFromTimezoneMap implements TimezoneFinder
} }
$tzidAlternate = $matches[3]; $tzidAlternate = $matches[3];
if ($this->hasTzInMap($tzidAlternate)) { if ($this->hasTzInMap($tzidAlternate)) {
return new \DateTimeZone($this->getTzFromMap($tzidAlternate)); try {
return new \DateTimeZone($this->getTzFromMap($tzidAlternate));
} catch (\Exception) {
return null;
}
} }
} }

View file

@ -23,7 +23,7 @@ class GuessFromLicEntry implements TimezoneGuesser
// Libical generators may specify strings like // Libical generators may specify strings like
// "SystemV/EST5EDT". For those we must remove the // "SystemV/EST5EDT". For those we must remove the
// SystemV part. // SystemV part.
if ('SystemV/' === substr($lic, 0, 8)) { if (str_starts_with($lic, 'SystemV/')) {
$lic = substr($lic, 8); $lic = substr($lic, 8);
} }

View file

@ -105,7 +105,7 @@ class GuessFromMsTzId implements TimezoneGuesser
$cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
// 2 can mean both Europe/Lisbon and Europe/Sarajevo. // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { if (2 === $cdoId && str_contains((string) $vtimezone->TZID, 'Sarajevo')) {
return new \DateTimeZone('Europe/Sarajevo'); return new \DateTimeZone('Europe/Sarajevo');
} }

View file

@ -38,10 +38,10 @@ class VCardConverter
return clone $input; return clone $input;
} }
if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) { if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40], true)) {
throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data');
} }
if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) { if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40], true)) {
throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version');
} }
@ -69,7 +69,7 @@ class VCardConverter
protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, int $targetVersion): void protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, int $targetVersion): void
{ {
// Skipping these, those are automatically added. // Skipping these, those are automatically added.
if (in_array($property->name, ['VERSION', 'PRODID'])) { if (in_array($property->name, ['VERSION', 'PRODID'], true)) {
return; return;
} }
@ -93,7 +93,7 @@ class VCardConverter
); );
if (Document::VCARD30 === $targetVersion) { if (Document::VCARD30 === $targetVersion) {
if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'], true)) {
/** @var Uri $newProperty */ /** @var Uri $newProperty */
$newProperty = $this->convertUriToBinary($output, $newProperty); $newProperty = $this->convertUriToBinary($output, $newProperty);
} elseif ($property instanceof Property\VCard\DateAndOrTime) { } elseif ($property instanceof Property\VCard\DateAndOrTime) {
@ -111,7 +111,7 @@ class VCardConverter
$newProperty['X-APPLE-OMIT-YEAR'] = '1604'; $newProperty['X-APPLE-OMIT-YEAR'] = '1604';
} }
if ('ANNIVERSARY' == $newProperty->name) { if ('ANNIVERSARY' === $newProperty->name) {
// Microsoft non-standard anniversary // Microsoft non-standard anniversary
$newProperty->name = 'X-ANNIVERSARY'; $newProperty->name = 'X-ANNIVERSARY';
@ -127,7 +127,7 @@ class VCardConverter
$output->add('ITEM'.$x.'.X-ABLABEL', '_$!<Anniversary>!$_'); $output->add('ITEM'.$x.'.X-ABLABEL', '_$!<Anniversary>!$_');
} }
} elseif ('KIND' === $property->name) { } elseif ('KIND' === $property->name) {
switch (strtolower($property->getValue())) { switch (strtolower((string) $property->getValue())) {
case 'org': case 'org':
// vCard 3.0 does not have an equivalent to KIND:ORG, // vCard 3.0 does not have an equivalent to KIND:ORG,
// but apple has an extension that means the same // but apple has an extension that means the same
@ -149,7 +149,7 @@ class VCardConverter
} }
} elseif (Document::VCARD40 === $targetVersion) { } elseif (Document::VCARD40 === $targetVersion) {
// These properties were removed in vCard 4.0 // These properties were removed in vCard 4.0
if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'], true)) {
return; return;
} }
@ -171,12 +171,12 @@ class VCardConverter
} }
switch ($property->name) { switch ($property->name) {
case 'X-ABSHOWAS': case 'X-ABSHOWAS':
if ('COMPANY' === strtoupper($property->getValue())) { if ('COMPANY' === strtoupper((string) $property->getValue())) {
$newProperty = $output->createProperty('KIND', 'ORG'); $newProperty = $output->createProperty('KIND', 'ORG');
} }
break; break;
case 'X-ADDRESSBOOKSERVER-KIND': case 'X-ADDRESSBOOKSERVER-KIND':
if ('GROUP' === strtoupper($property->getValue())) { if ('GROUP' === strtoupper((string) $property->getValue())) {
$newProperty = $output->createProperty('KIND', 'GROUP'); $newProperty = $output->createProperty('KIND', 'GROUP');
} }
break; break;
@ -274,10 +274,11 @@ class VCardConverter
$newTypes = []; $newTypes = [];
foreach ($parameters['TYPE']->getParts() as $typePart) { foreach ($parameters['TYPE']->getParts() as $typePart) {
if (in_array( if (in_array(
strtoupper($typePart), strtoupper((string) $typePart),
['JPEG', 'PNG', 'GIF'] ['JPEG', 'PNG', 'GIF'],
true
)) { )) {
$mimeType = 'image/'.strtolower($typePart); $mimeType = 'image/'.strtolower((string) $typePart);
} else { } else {
$newTypes[] = $typePart; $newTypes[] = $typePart;
} }
@ -292,7 +293,7 @@ class VCardConverter
} }
} }
$newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value)); $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode((string) $value));
return $newProperty; return $newProperty;
} }
@ -313,7 +314,7 @@ class VCardConverter
$value = $newProperty->getValue(); $value = $newProperty->getValue();
// Only converting data: uris // Only converting data: uris
if ('data:' !== substr($value, 0, 5)) { if (!str_starts_with((string) $value, 'data:')) {
return $newProperty; return $newProperty;
} }
@ -325,12 +326,12 @@ class VCardConverter
'BINARY' 'BINARY'
); );
$mimeType = substr($value, 5, strpos($value, ',') - 5); $mimeType = substr((string) $value, 5, strpos((string) $value, ',') - 5);
if (strpos($mimeType, ';')) { if (strpos($mimeType, ';')) {
$mimeType = substr($mimeType, 0, strpos($mimeType, ';')); $mimeType = substr($mimeType, 0, strpos($mimeType, ';'));
$newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); $newProperty->setValue(base64_decode(substr((string) $value, strpos((string) $value, ',') + 1), true));
} else { } else {
$newProperty->setValue(substr($value, strpos($value, ',') + 1)); $newProperty->setValue(substr((string) $value, strpos((string) $value, ',') + 1));
} }
unset($value); unset($value);
@ -367,7 +368,7 @@ class VCardConverter
// that's now PREF=1. // that's now PREF=1.
case 'TYPE': case 'TYPE':
foreach ($param->getParts() as $paramPart) { foreach ($param->getParts() as $paramPart) {
if ('PREF' === strtoupper($paramPart)) { if ('PREF' === strtoupper((string) $paramPart)) {
$newProperty->add('PREF', '1'); $newProperty->add('PREF', '1');
} else { } else {
$newProperty->add($param->name, $paramPart); $newProperty->add($param->name, $paramPart);
@ -402,7 +403,7 @@ class VCardConverter
case 'ENCODING': case 'ENCODING':
// This value only existed in vCard 2.1, and should be // This value only existed in vCard 2.1, and should be
// removed for anything else. // removed for anything else.
if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) { if ('QUOTED-PRINTABLE' !== strtoupper((string) $param->getValue())) {
$newProperty->add($param->name, $param->getParts()); $newProperty->add($param->name, $param->getParts());
} }
break; break;
@ -413,7 +414,7 @@ class VCardConverter
* Any other PREF numbers we'll drop. * Any other PREF numbers we'll drop.
*/ */
case 'PREF': case 'PREF':
if ('1' == $param->getValue()) { if ('1' === (string) $param->getValue()) {
$newProperty->add('TYPE', 'PREF'); $newProperty->add('TYPE', 'PREF');
} }
break; break;

View file

@ -14,5 +14,5 @@ class Version
/** /**
* Full version number. * Full version number.
*/ */
public const VERSION = '4.5.2'; public const VERSION = '5.0.0';
} }

View file

@ -17,6 +17,7 @@ return [
'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London',
'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
'Amsterdam, Berlin, Bern, Rom, Stockholm, Wien' => 'Europe/Berlin',
'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
@ -39,12 +40,12 @@ return [
'Kabul' => 'Asia/Kabul', 'Kabul' => 'Asia/Kabul',
'Ekaterinburg' => 'Asia/Yekaterinburg', 'Ekaterinburg' => 'Asia/Yekaterinburg',
'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Kolkata',
'Kathmandu, Nepal' => 'Asia/Kathmandu', 'Kathmandu, Nepal' => 'Asia/Kathmandu',
'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
'Astana, Dhaka' => 'Asia/Dhaka', 'Astana, Dhaka' => 'Asia/Dhaka',
'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
'Rangoon' => 'Asia/Rangoon', 'Rangoon' => 'Asia/Yangon',
'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
'Krasnoyarsk' => 'Asia/Krasnoyarsk', 'Krasnoyarsk' => 'Asia/Krasnoyarsk',
'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
@ -71,7 +72,7 @@ return [
'Mid-Atlantic' => 'America/Noronha', 'Mid-Atlantic' => 'America/Noronha',
'Brasilia' => 'America/Sao_Paulo', // Best guess 'Brasilia' => 'America/Sao_Paulo', // Best guess
'Buenos Aires' => 'America/Argentina/Buenos_Aires', 'Buenos Aires' => 'America/Argentina/Buenos_Aires',
'Greenland' => 'America/Godthab', 'Greenland' => 'America/Nuuk',
'Newfoundland' => 'America/St_Johns', 'Newfoundland' => 'America/St_Johns',
'Atlantic Time (Canada)' => 'America/Halifax', 'Atlantic Time (Canada)' => 'America/Halifax',
'Caracas, La Paz' => 'America/Caracas', 'Caracas, La Paz' => 'America/Caracas',

View file

@ -34,7 +34,7 @@ return [
'Newfoundland' => 'America/St_Johns', 'Newfoundland' => 'America/St_Johns',
'Argentina' => 'America/Argentina/Buenos_Aires', 'Argentina' => 'America/Argentina/Buenos_Aires',
'E. South America' => 'America/Belem', 'E. South America' => 'America/Belem',
'Greenland' => 'America/Godthab', 'Greenland' => 'America/Nuuk',
'Montevideo' => 'America/Montevideo', 'Montevideo' => 'America/Montevideo',
'SA Eastern' => 'America/Belem', 'SA Eastern' => 'America/Belem',
// 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones.
@ -71,12 +71,12 @@ return [
'Ekaterinburg' => 'Asia/Yekaterinburg', 'Ekaterinburg' => 'Asia/Yekaterinburg',
'Pakistan' => 'Asia/Karachi', 'Pakistan' => 'Asia/Karachi',
'West Asia' => 'Asia/Tashkent', 'West Asia' => 'Asia/Tashkent',
'India' => 'Asia/Calcutta', 'India' => 'Asia/Kolkata',
'Sri Lanka' => 'Asia/Colombo', 'Sri Lanka' => 'Asia/Colombo',
'Nepal' => 'Asia/Kathmandu', 'Nepal' => 'Asia/Kathmandu',
'Central Asia' => 'Asia/Dhaka', 'Central Asia' => 'Asia/Dhaka',
'N. Central Asia' => 'Asia/Almaty', 'N. Central Asia' => 'Asia/Almaty',
'Myanmar' => 'Asia/Rangoon', 'Myanmar' => 'Asia/Yangon',
'North Asia' => 'Asia/Krasnoyarsk', 'North Asia' => 'Asia/Krasnoyarsk',
'SE Asia' => 'Asia/Bangkok', 'SE Asia' => 'Asia/Bangkok',
'China' => 'Asia/Shanghai', 'China' => 'Asia/Shanghai',

View file

@ -20,7 +20,7 @@ return [
'Arab Standard Time' => 'Asia/Riyadh', 'Arab Standard Time' => 'Asia/Riyadh',
'Arabian Standard Time' => 'Asia/Dubai', 'Arabian Standard Time' => 'Asia/Dubai',
'Arabic Standard Time' => 'Asia/Baghdad', 'Arabic Standard Time' => 'Asia/Baghdad',
'Argentina Standard Time' => 'America/Buenos_Aires', 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires',
'Astrakhan Standard Time' => 'Europe/Astrakhan', 'Astrakhan Standard Time' => 'Europe/Astrakhan',
'Atlantic Standard Time' => 'America/Halifax', 'Atlantic Standard Time' => 'America/Halifax',
'Aus Central W. Standard Time' => 'Australia/Eucla', 'Aus Central W. Standard Time' => 'Australia/Eucla',
@ -55,16 +55,16 @@ return [
'Eastern Standard Time (Mexico)' => 'America/Cancun', 'Eastern Standard Time (Mexico)' => 'America/Cancun',
'Egypt Standard Time' => 'Africa/Cairo', 'Egypt Standard Time' => 'Africa/Cairo',
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
'FLE Standard Time' => 'Europe/Kiev', 'FLE Standard Time' => 'Europe/Kyiv',
'Fiji Standard Time' => 'Pacific/Fiji', 'Fiji Standard Time' => 'Pacific/Fiji',
'GMT Standard Time' => 'Europe/London', 'GMT Standard Time' => 'Europe/London',
'GTB Standard Time' => 'Europe/Bucharest', 'GTB Standard Time' => 'Europe/Bucharest',
'Georgian Standard Time' => 'Asia/Tbilisi', 'Georgian Standard Time' => 'Asia/Tbilisi',
'Greenland Standard Time' => 'America/Godthab', 'Greenland Standard Time' => 'America/Nuuk',
'Greenwich Standard Time' => 'Atlantic/Reykjavik', 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
'Haiti Standard Time' => 'America/Port-au-Prince', 'Haiti Standard Time' => 'America/Port-au-Prince',
'Hawaiian Standard Time' => 'Pacific/Honolulu', 'Hawaiian Standard Time' => 'Pacific/Honolulu',
'India Standard Time' => 'Asia/Calcutta', 'India Standard Time' => 'Asia/Kolkata',
'Iran Standard Time' => 'Asia/Tehran', 'Iran Standard Time' => 'Asia/Tehran',
'Israel Standard Time' => 'Asia/Jerusalem', 'Israel Standard Time' => 'Asia/Jerusalem',
'Jordan Standard Time' => 'Asia/Amman', 'Jordan Standard Time' => 'Asia/Amman',
@ -82,10 +82,10 @@ return [
'Morocco Standard Time' => 'Africa/Casablanca', 'Morocco Standard Time' => 'Africa/Casablanca',
'Mountain Standard Time' => 'America/Denver', 'Mountain Standard Time' => 'America/Denver',
'Mountain Standard Time (Mexico)' => 'America/Chihuahua', 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
'Myanmar Standard Time' => 'Asia/Rangoon', 'Myanmar Standard Time' => 'Asia/Yangon',
'N. Central Asia Standard Time' => 'Asia/Novosibirsk', 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
'Namibia Standard Time' => 'Africa/Windhoek', 'Namibia Standard Time' => 'Africa/Windhoek',
'Nepal Standard Time' => 'Asia/Katmandu', 'Nepal Standard Time' => 'Asia/Kathmandu',
'New Zealand Standard Time' => 'Pacific/Auckland', 'New Zealand Standard Time' => 'Pacific/Auckland',
'Newfoundland Standard Time' => 'America/St_Johns', 'Newfoundland Standard Time' => 'America/St_Johns',
'Norfolk Standard Time' => 'Pacific/Norfolk', 'Norfolk Standard Time' => 'Pacific/Norfolk',
@ -127,7 +127,7 @@ return [
'Transbaikal Standard Time' => 'Asia/Chita', 'Transbaikal Standard Time' => 'Asia/Chita',
'Turkey Standard Time' => 'Europe/Istanbul', 'Turkey Standard Time' => 'Europe/Istanbul',
'Turks And Caicos Standard Time' => 'America/Grand_Turk', 'Turks And Caicos Standard Time' => 'America/Grand_Turk',
'US Eastern Standard Time' => 'America/Indianapolis', 'US Eastern Standard Time' => 'America/Indiana/Indianapolis',
'US Mountain Standard Time' => 'America/Phoenix', 'US Mountain Standard Time' => 'America/Phoenix',
'UTC' => 'Etc/GMT', 'UTC' => 'Etc/GMT',
'UTC+12' => 'Etc/GMT-12', 'UTC+12' => 'Etc/GMT-12',