Upgrade Sabre #304 (needs testing)

This commit is contained in:
the-djmaze 2022-03-24 12:02:45 +01:00
parent 2fae820ee4
commit b074a9eaf3
105 changed files with 12726 additions and 4587 deletions

View file

@ -115,6 +115,7 @@ trait Raw
private function rawSmart(bool $bDownload, bool $bThumbnail = false) : bool private function rawSmart(bool $bDownload, bool $bThumbnail = false) : bool
{ {
$sRawKey = (string) $this->GetActionParam('RawKey', ''); $sRawKey = (string) $this->GetActionParam('RawKey', '');
$aValues = $this->getDecodedRawKeyValue($sRawKey); $aValues = $this->getDecodedRawKeyValue($sRawKey);
$sRange = $this->Http()->GetHeader('Range'); $sRange = $this->Http()->GetHeader('Range');
@ -139,8 +140,6 @@ trait Raw
$sFileNameIn = isset($aValues['FileName']) ? (string) $aValues['FileName'] : ''; $sFileNameIn = isset($aValues['FileName']) ? (string) $aValues['FileName'] : '';
$sFileHashIn = isset($aValues['FileHash']) ? (string) $aValues['FileHash'] : ''; $sFileHashIn = isset($aValues['FileHash']) ? (string) $aValues['FileHash'] : '';
$bDetectImageOrientation = !!$this->Config()->Get('labs', 'detect_image_exif_orientation', false);
if (!empty($sFileHashIn)) if (!empty($sFileHashIn))
{ {
$this->verifyCacheByKey($sRawKey); $this->verifyCacheByKey($sRawKey);
@ -172,60 +171,49 @@ trait Raw
return false; return false;
} }
else else if (!empty($sFolder) && 0 < $iUid)
{
if (!empty($sFolder) && 0 < $iUid)
{ {
$this->verifyCacheByKey($sRawKey); $this->verifyCacheByKey($sRawKey);
} }
}
$oAccount = $this->initMailClientConnection(); $oAccount = $this->initMailClientConnection();
$self = $this; $self = $this;
return $this->MailClient()->MessageMimeStream( return $this->MailClient()->MessageMimeStream(
function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ( function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use (
$self, $oAccount, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail, $bDetectImageOrientation, $self, $oAccount, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail,
$bIsRangeRequest, $sRangeStart, $sRangeEnd $bIsRangeRequest, $sRangeStart, $sRangeEnd
) { ) {
if ($oAccount && \is_resource($rResource)) if ($oAccount && \is_resource($rResource))
{ {
\MailSo\Base\Utils::ResetTimeLimit(); \MailSo\Base\Utils::ResetTimeLimit();
$sContentTypeOut = $sContentTypeIn;
if (empty($sContentTypeOut))
{
$sContentTypeOut = $sContentType;
if (empty($sContentTypeOut))
{
$sContentTypeOut = (empty($sFileName)) ? 'text/plain' : \MailSo\Base\Utils::MimeContentType($sFileName);
}
}
$sFileNameOut = $sFileNameIn;
if (empty($sFileNameOut))
{
$sFileNameOut = $sFileName;
}
// https://github.com/the-djmaze/snappymail/issues/144
if ('.pdf' === \substr($sFileNameOut,-4)) {
$sContentTypeOut = 'application/pdf';
}
$sFileNameOut = $self->MainClearFileName($sFileNameOut, $sContentTypeOut, $sMimeIndex);
$self->cacheByKey($sRawKey); $self->cacheByKey($sRawKey);
if ($sFileNameIn) {
$sFileName = $sFileNameIn;
}
$sFileName = $self->MainClearFileName($sFileName, $sContentType, $sMimeIndex);
if ('.pdf' === \substr($sFileName, -4)) {
// https://github.com/the-djmaze/snappymail/issues/144
$sContentType = 'application/pdf';
} else if ($sContentTypeIn) {
$sContentType = $sContentTypeIn;
} else if (!$sContentType) {
$sContentType = $sFileName ? \MailSo\Base\Utils::MimeContentType($sFileName) : 'text/plain';
}
if (!$bDownload) if (!$bDownload)
{ {
$bDetectImageOrientation = !!$self->Config()->Get('labs', 'detect_image_exif_orientation', false);
if ($bThumbnail) if ($bThumbnail)
{ {
try try
{ {
$oImage = static::loadImage($rResource, $bDetectImageOrientation, 60); $oImage = static::loadImage($rResource, $bDetectImageOrientation, 60);
\header('Content-Disposition: inline; '. \header('Content-Disposition: inline; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut.'_thumb60x60.png'))); \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileName.'_thumb60x60.png')));
$oImage->show('png'); $oImage->show('png');
// $oImage->show('webp'); // Little Britain: "Safari says NO" // $oImage->show('webp'); // Little Britain: "Safari says NO"
} }
@ -236,16 +224,15 @@ trait Raw
exit; exit;
} }
else if ($bDetectImageOrientation && else if ($bDetectImageOrientation &&
\in_array($sContentTypeOut, array('image/png', 'image/jpeg', 'image/jpg', 'image/webp'))) \in_array($sContentType, array('image/png', 'image/jpeg', 'image/jpg', 'image/webp')))
{ {
try try
{ {
$oImage = static::loadImage($rResource, $bDetectImageOrientation); $oImage = static::loadImage($rResource, $bDetectImageOrientation);
\header('Content-Disposition: inline; '. \header('Content-Disposition: inline; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut))); \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileName)));
$oImage->show(); $oImage->show();
// $oImage->show('webp'); // Little Britain: "Safari says NO" // $oImage->show('webp'); // Little Britain: "Safari says NO"
exit;
} }
catch (\Throwable $oException) catch (\Throwable $oException)
{ {
@ -256,9 +243,9 @@ trait Raw
} }
if (!\headers_sent()) { if (!\headers_sent()) {
\header('Content-Type: '.$sContentTypeOut); \header('Content-Type: '.$sContentType);
\header('Content-Disposition: '.($bDownload ? 'attachment' : 'inline').'; '. \header('Content-Disposition: '.($bDownload ? 'attachment' : 'inline').'; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut))); \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileName)));
\header('Accept-Ranges: bytes'); \header('Accept-Ranges: bytes');
\header('Content-Transfer-Encoding: binary'); \header('Content-Transfer-Encoding: binary');

View file

@ -1,4 +1,4 @@
Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). Copyright (C) 2011-2016 fruux GmbH (https://fruux.com/)
All rights reserved. All rights reserved.
@ -10,7 +10,7 @@ are permitted provided that the following conditions are met:
* Redistributions in binary form must reproduce the above copyright notice, * Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
* Neither the name of SabreDAV nor the names of its contributors * Neither the name Sabre nor the names of its contributors
may be used to endorse or promote products derived from this software may be used to endorse or promote products derived from this software
without specific prior written permission. without specific prior written permission.

View file

@ -0,0 +1,172 @@
<?php
namespace Sabre\VObject;
use Sabre\VObject\Component\VCalendar;
/**
* This class generates birthday calendars.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Dominik Tobschall (http://tobschall.de/)
* @license http://sabre.io/license/ Modified BSD License
*/
class BirthdayCalendarGenerator
{
/**
* Input objects.
*
* @var array
*/
protected $objects = [];
/**
* Default year.
* Used for dates without a year.
*/
const DEFAULT_YEAR = 2000;
/**
* Output format for the SUMMARY.
*
* @var string
*/
protected $format = '%1$s\'s Birthday';
/**
* Creates the generator.
*
* Check the setTimeRange and setObjects methods for details about the
* arguments.
*
* @param mixed $objects
*/
public function __construct($objects = null)
{
if ($objects) {
$this->setObjects($objects);
}
}
/**
* Sets the input objects.
*
* You must either supply a vCard as a string or as a Component/VCard object.
* It's also possible to supply an array of strings or objects.
*
* @param mixed $objects
*/
public function setObjects($objects)
{
if (!is_array($objects)) {
$objects = [$objects];
}
$this->objects = [];
foreach ($objects as $object) {
if (is_string($object)) {
$vObj = Reader::read($object);
if (!$vObj instanceof Component\VCard) {
throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects');
}
$this->objects[] = $vObj;
} elseif ($object instanceof Component\VCard) {
$this->objects[] = $object;
} else {
throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects');
}
}
}
/**
* Sets the output format for the SUMMARY.
*
* @param string $format
*/
public function setFormat($format)
{
$this->format = $format;
}
/**
* Parses the input data and returns a VCALENDAR.
*
* @return Component/VCalendar
*/
public function getResult()
{
$calendar = new VCalendar();
foreach ($this->objects as $object) {
// Skip if there is no BDAY property.
if (!$object->select('BDAY')) {
continue;
}
// We've seen clients (ez-vcard) putting "BDAY:" properties
// without a value into vCards. If we come across those, we'll
// skip them.
if (empty($object->BDAY->getValue())) {
continue;
}
// We're always converting to vCard 4.0 so we can rely on the
// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
$object = $object->convert(Document::VCARD40);
// Skip if the card has no FN property.
if (!isset($object->FN)) {
continue;
}
// Skip if the BDAY property is not of the right type.
if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) {
continue;
}
// Skip if we can't parse the BDAY value.
try {
$dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue());
} catch (InvalidDataException $e) {
continue;
}
// Set a year if it's not set.
$unknownYear = false;
if (!$dateParts['year']) {
$object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date'];
$unknownYear = true;
}
// Create event.
$event = $calendar->add('VEVENT', [
'SUMMARY' => sprintf($this->format, $object->FN->getValue()),
'DTSTART' => new \DateTime($object->BDAY->getValue()),
'RRULE' => 'FREQ=YEARLY',
'TRANSP' => 'TRANSPARENT',
]);
// add VALUE=date
$event->DTSTART['VALUE'] = 'DATE';
// Add X-SABRE-BDAY property.
if ($unknownYear) {
$event->add('X-SABRE-BDAY', 'BDAY', [
'X-SABRE-VCARD-UID' => $object->UID->getValue(),
'X-SABRE-VCARD-FN' => $object->FN->getValue(),
'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR,
]);
} else {
$event->add('X-SABRE-BDAY', 'BDAY', [
'X-SABRE-VCARD-UID' => $object->UID->getValue(),
'X-SABRE-VCARD-FN' => $object->FN->getValue(),
]);
}
}
return $calendar;
}
}

View file

@ -2,83 +2,82 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use use InvalidArgumentException;
InvalidArgumentException;
/** /**
* This is the CLI interface for sabre-vobject. * This is the CLI interface for sabre-vobject.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Cli { class Cli
{
/** /**
* No output * No output.
* *
* @var bool * @var bool
*/ */
protected $quiet = false; protected $quiet = false;
/** /**
* Help display * Help display.
* *
* @var bool * @var bool
*/ */
protected $showHelp = false; protected $showHelp = false;
/** /**
* Wether to spit out 'mimedir' or 'json' format. * Whether to spit out 'mimedir' or 'json' format.
* *
* @var string * @var string
*/ */
protected $format; protected $format;
/** /**
* JSON pretty print * JSON pretty print.
* *
* @var bool * @var bool
*/ */
protected $pretty; protected $pretty;
/** /**
* Source file * Source file.
* *
* @var string * @var string
*/ */
protected $inputPath; protected $inputPath;
/** /**
* Destination file * Destination file.
* *
* @var string * @var string
*/ */
protected $outputPath; protected $outputPath;
/** /**
* output stream * output stream.
* *
* @var resource * @var resource
*/ */
protected $stdout; protected $stdout;
/** /**
* stdin * stdin.
* *
* @var resource * @var resource
*/ */
protected $stdin; protected $stdin;
/** /**
* stderr * stderr.
* *
* @var resource * @var resource
*/ */
protected $stderr; protected $stderr;
/** /**
* Input format (one of json or mimedir) * Input format (one of json or mimedir).
* *
* @var string * @var string
*/ */
@ -92,66 +91,59 @@ class Cli {
protected $forgiving = false; protected $forgiving = false;
/** /**
* Main function * Main function.
* *
* @return int * @return int
*/ */
public function main(array $argv) { public function main(array $argv)
{
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
// We cannot easily test this, so we'll skip it. Pretty basic anyway. // We cannot easily test this, so we'll skip it. Pretty basic anyway.
if (!$this->stderr) { if (!$this->stderr) {
$this->stderr = STDERR; $this->stderr = fopen('php://stderr', 'w');
} }
if (!$this->stdout) { if (!$this->stdout) {
$this->stdout = STDOUT; $this->stdout = fopen('php://stdout', 'w');
} }
if (!$this->stdin) { if (!$this->stdin) {
$this->stdin = STDIN; $this->stdin = fopen('php://stdin', 'r');
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
try { try {
list($options, $positional) = $this->parseArguments($argv); list($options, $positional) = $this->parseArguments($argv);
if (isset($options['q'])) { if (isset($options['q'])) {
$this->quiet = true; $this->quiet = true;
} }
$this->log($this->colorize('green', "sabre-vobject ") . $this->colorize('yellow', Version::VERSION)); $this->log($this->colorize('green', 'sabre/vobject ').$this->colorize('yellow', Version::VERSION));
foreach ($options as $name => $value) { foreach ($options as $name => $value) {
switch ($name) { switch ($name) {
case 'q': case 'q':
// Already handled earlier. // Already handled earlier.
break; break;
case 'h': case 'h':
case 'help': case 'help':
$this->showHelp(); $this->showHelp();
return 0; return 0;
break; break;
case 'format': case 'format':
switch ($value) { switch ($value) {
// jcard/jcal documents // jcard/jcal documents
case 'jcard': case 'jcard':
case 'jcal': case 'jcal':
// specific document versions // specific document versions
case 'vcard21': case 'vcard21':
case 'vcard30': case 'vcard30':
case 'vcard40': case 'vcard40':
case 'icalendar20': case 'icalendar20':
// specific formats // specific formats
case 'json': case 'json':
case 'mimedir': case 'mimedir':
// icalendar/vcad // icalendar/vcad
case 'icalendar': case 'icalendar':
case 'vcard': case 'vcard':
@ -160,11 +152,12 @@ class Cli {
default: default:
throw new InvalidArgumentException('Unknown format: '.$value); throw new InvalidArgumentException('Unknown format: '.$value);
} }
break; break;
case 'pretty': case 'pretty':
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
$this->pretty = true; $this->pretty = true;
}
break; break;
case 'forgiving': case 'forgiving':
$this->forgiving = true; $this->forgiving = true;
@ -186,28 +179,25 @@ class Cli {
case 'vcard30': case 'vcard30':
case 'vcard40': case 'vcard40':
case 'icalendar20': case 'icalendar20':
$this->inputFormat = 'mimedir'; $this->inputFormat = 'mimedir';
break; break;
default: default:
throw new InvalidArgumentException('Unknown format: '.$value); throw new InvalidArgumentException('Unknown format: '.$value);
} }
break; break;
default: default:
throw new InvalidArgumentException('Unknown option: '.$name); throw new InvalidArgumentException('Unknown option: '.$name);
}
} }
} if (0 === count($positional)) {
if (count($positional) === 0) {
$this->showHelp(); $this->showHelp();
return 1; return 1;
} }
if (count($positional) === 1) { if (1 === count($positional)) {
throw new InvalidArgumentException('Inputfile is a required argument'); throw new InvalidArgumentException('Inputfile is a required argument');
} }
@ -215,13 +205,13 @@ class Cli {
throw new InvalidArgumentException('Too many arguments'); throw new InvalidArgumentException('Too many arguments');
} }
if (!in_array($positional[0], array('validate','repair','convert','color'))) { if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) {
throw new InvalidArgumentException('Uknown command: ' . $positional[0]); throw new InvalidArgumentException('Unknown command: '.$positional[0]);
} }
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$this->showHelp(); $this->showHelp();
$this->log('Error: '.$e->getMessage(), 'red'); $this->log('Error: '.$e->getMessage(), 'red');
return 1; return 1;
} }
@ -230,74 +220,73 @@ class Cli {
$this->inputPath = $positional[1]; $this->inputPath = $positional[1];
$this->outputPath = isset($positional[2]) ? $positional[2] : '-'; $this->outputPath = isset($positional[2]) ? $positional[2] : '-';
if ($this->outputPath !== '-') { if ('-' !== $this->outputPath) {
$this->stdout = fopen($this->outputPath, 'w'); $this->stdout = fopen($this->outputPath, 'w');
} }
if (!$this->inputFormat) { if (!$this->inputFormat) {
if (substr($this->inputPath,-5)==='.json') { if ('.json' === substr($this->inputPath, -5)) {
$this->inputFormat = 'json'; $this->inputFormat = 'json';
} else { } else {
$this->inputFormat = 'mimedir'; $this->inputFormat = 'mimedir';
} }
} }
if (!$this->format) { if (!$this->format) {
if (substr($this->outputPath,-5)==='.json') { if ('.json' === substr($this->outputPath, -5)) {
$this->format = 'json'; $this->format = 'json';
} else { } else {
$this->format = 'mimedir'; $this->format = 'mimedir';
} }
} }
$realCode = 0; $realCode = 0;
try { try {
while ($input = $this->readInput()) { while ($input = $this->readInput()) {
$returnCode = $this->$command($input); $returnCode = $this->$command($input);
if ($returnCode!==0) $realCode = $returnCode; if (0 !== $returnCode) {
$realCode = $returnCode;
}
} }
} catch (EofException $e) { } catch (EofException $e) {
// end of file // end of file
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log('Error: '.$e->getMessage(), 'red'); $this->log('Error: '.$e->getMessage(), 'red');
return 2; return 2;
} }
return $realCode; return $realCode;
} }
/** /**
* Shows the help message. * Shows the help message.
*
* @return void
*/ */
protected function showHelp() { protected function showHelp()
{
$this->log('Usage:', 'yellow'); $this->log('Usage:', 'yellow');
$this->log(" vobject [options] command [arguments]"); $this->log(' vobject [options] command [arguments]');
$this->log(''); $this->log('');
$this->log('Options:', 'yellow'); $this->log('Options:', 'yellow');
$this->log($this->colorize('green', ' -q ')."Don't output anything."); $this->log($this->colorize('green', ' -q ')."Don't output anything.");
$this->log($this->colorize('green', ' -help -h ') . "Display this help message."); $this->log($this->colorize('green', ' -help -h ').'Display this help message.');
$this->log($this->colorize('green', ' --format ') . "Convert to a specific format. Must be one of: vcard, vcard21,"); $this->log($this->colorize('green', ' --format ').'Convert to a specific format. Must be one of: vcard, vcard21,');
$this->log($this->colorize('green', ' --forgiving ') . "Makes the parser less strict."); $this->log($this->colorize('green', ' --forgiving ').'Makes the parser less strict.');
$this->log(" vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir."); $this->log(' vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir.');
$this->log($this->colorize('green', ' --inputformat ') . "If the input format cannot be guessed from the extension, it"); $this->log($this->colorize('green', ' --inputformat ').'If the input format cannot be guessed from the extension, it');
$this->log(" must be specified here."); $this->log(' must be specified here.');
$this->log($this->colorize('green', ' --pretty ') . "json pretty-print."); // Only PHP 5.4 and up
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
$this->log($this->colorize('green', ' --pretty ').'json pretty-print.');
}
$this->log(''); $this->log('');
$this->log('Commands:', 'yellow'); $this->log('Commands:', 'yellow');
$this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.'); $this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.');
$this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.'); $this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.');
$this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.'); $this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.');
$this->log($this->colorize('green', ' color') . ' source_file Colorize a file, useful for debbugging.'); $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.');
$this->log(<<<HELP $this->log(
<<<HELP
If source_file is set as '-', STDIN will be used. If source_file is set as '-', STDIN will be used.
If output_file is omitted, STDOUT will be used. If output_file is omitted, STDOUT will be used.
@ -313,91 +302,98 @@ HELP
$this->log(' vobject color calendar.ics'); $this->log(' vobject color calendar.ics');
$this->log(''); $this->log('');
$this->log('https://github.com/fruux/sabre-vobject', 'purple'); $this->log('https://github.com/fruux/sabre-vobject', 'purple');
} }
/** /**
* Validates a VObject file * Validates a VObject file.
* *
* @param Component $vObj
* @return int * @return int
*/ */
protected function validate($vObj) { protected function validate(Component $vObj)
{
$returnCode = 0; $returnCode = 0;
switch ($vObj->name) { switch ($vObj->name) {
case 'VCALENDAR': case 'VCALENDAR':
$this->log("iCalendar: " . (string)$vObj->VERSION); $this->log('iCalendar: '.(string) $vObj->VERSION);
break; break;
case 'VCARD': case 'VCARD':
$this->log("vCard: " . (string)$vObj->VERSION); $this->log('vCard: '.(string) $vObj->VERSION);
break; break;
} }
$warnings = $vObj->validate(); $warnings = $vObj->validate();
if (!count($warnings)) { if (!count($warnings)) {
$this->log(" No warnings!"); $this->log(' No warnings!');
} else { } else {
$levels = [
1 => 'REPAIRED',
2 => 'WARNING',
3 => 'ERROR',
];
$returnCode = 2; $returnCode = 2;
foreach ($warnings as $warn) { foreach ($warnings as $warn) {
$extra = '';
$this->log(" " . $warn['message']); if ($warn['node'] instanceof Property) {
$extra = ' (property: "'.$warn['node']->name.'")';
}
$this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra);
} }
} }
return $returnCode; return $returnCode;
} }
/** /**
* Repairs a VObject file * Repairs a VObject file.
* *
* @param Component $vObj
* @return int * @return int
*/ */
protected function repair($vObj) { protected function repair(Component $vObj)
{
$returnCode = 0; $returnCode = 0;
switch ($vObj->name) { switch ($vObj->name) {
case 'VCALENDAR': case 'VCALENDAR':
$this->log("iCalendar: " . (string)$vObj->VERSION); $this->log('iCalendar: '.(string) $vObj->VERSION);
break; break;
case 'VCARD': case 'VCARD':
$this->log("vCard: " . (string)$vObj->VERSION); $this->log('vCard: '.(string) $vObj->VERSION);
break; break;
} }
$warnings = $vObj->validate(Node::REPAIR); $warnings = $vObj->validate(Node::REPAIR);
if (!count($warnings)) { if (!count($warnings)) {
$this->log(" No warnings!"); $this->log(' No warnings!');
} else { } else {
foreach($warnings as $warn) { $levels = [
1 => 'REPAIRED',
2 => 'WARNING',
3 => 'ERROR',
];
$returnCode = 2; $returnCode = 2;
$this->log(" " . $warn['message']); foreach ($warnings as $warn) {
$extra = '';
if ($warn['node'] instanceof Property) {
$extra = ' (property: "'.$warn['node']->name.'")';
}
$this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra);
} }
} }
fwrite($this->stdout, $vObj->serialize()); fwrite($this->stdout, $vObj->serialize());
return $returnCode; return $returnCode;
} }
/** /**
* Converts a vObject file to a new format. * Converts a vObject file to a new format.
* *
* @param Component $vObj * @param Component $vObj
*
* @return int * @return int
*/ */
protected function convert($vObj) { protected function convert($vObj)
{
$json = false; $json = false;
$convertVersion = null; $convertVersion = null;
$forceInput = null; $forceInput = null;
@ -405,7 +401,7 @@ HELP
switch ($this->format) { switch ($this->format) {
case 'json': case 'json':
$json = true; $json = true;
if ($vObj->name === 'VCARD') { if ('VCARD' === $vObj->name) {
$convertVersion = Document::VCARD40; $convertVersion = Document::VCARD40;
} }
break; break;
@ -432,7 +428,6 @@ HELP
case 'vcard40': case 'vcard40':
$convertVersion = Document::VCARD40; $convertVersion = Document::VCARD40;
break; break;
} }
if ($forceInput && $vObj->name !== $forceInput) { if ($forceInput && $vObj->name !== $forceInput) {
@ -452,30 +447,28 @@ HELP
} }
return 0; return 0;
} }
/** /**
* Colorizes a file * Colorizes a file.
* *
* @param Component $vObj * @param Component $vObj
* @return int
*/ */
protected function color($vObj) { protected function color($vObj)
{
fwrite($this->stdout, $this->serializeComponent($vObj)); fwrite($this->stdout, $this->serializeComponent($vObj));
} }
/** /**
* Returns an ansi color string for a color name. * Returns an ansi color string for a color name.
* *
* @param string $color * @param string $color
*
* @return string * @return string
*/ */
protected function colorize($color, $str, $resetTo = 'default') { protected function colorize($color, $str, $resetTo = 'default')
{
$colors = array( $colors = [
'cyan' => '1;36', 'cyan' => '1;36',
'red' => '1;31', 'red' => '1;31',
'yellow' => '1;33', 'yellow' => '1;33',
@ -483,9 +476,9 @@ HELP
'green' => '0;32', 'green' => '0;32',
'default' => '0', 'default' => '0',
'purple' => '0;35', 'purple' => '0;35',
); ];
return "\033[" . $colors[$color] . 'm' . $str . "\033[".$colors[$resetTo]."m";
return "\033[".$colors[$color].'m'.$str."\033[".$colors[$resetTo].'m';
} }
/** /**
@ -493,16 +486,14 @@ HELP
* *
* @param string $color * @param string $color
* @param string $str * @param string $str
* @return void
*/ */
protected function cWrite($color, $str) { protected function cWrite($color, $str)
{
fwrite($this->stdout, $this->colorize($color, $str)); fwrite($this->stdout, $this->colorize($color, $str));
} }
protected function serializeComponent(Component $vObj) { protected function serializeComponent(Component $vObj)
{
$this->cWrite('cyan', 'BEGIN'); $this->cWrite('cyan', 'BEGIN');
$this->cWrite('red', ':'); $this->cWrite('red', ':');
$this->cWrite('yellow', $vObj->name."\n"); $this->cWrite('yellow', $vObj->name."\n");
@ -514,54 +505,58 @@ HELP
* *
* A higher score means the item will be lower in the list. * A higher score means the item will be lower in the list.
* To avoid score collisions, each "score category" has a reasonable * To avoid score collisions, each "score category" has a reasonable
* space to accomodate elements. The $key is added to the $score to * space to accommodate elements. The $key is added to the $score to
* preserve the original relative order of elements. * preserve the original relative order of elements.
* *
* @param int $key * @param int $key
* @param array $array * @param array $array
*
* @return int * @return int
*/ */
$sortScore = function ($key, $array) { $sortScore = function ($key, $array) {
if ($array[$key] instanceof Component) { if ($array[$key] instanceof Component) {
// We want to encode VTIMEZONE first, this is a personal // We want to encode VTIMEZONE first, this is a personal
// preference. // preference.
if ($array[$key]->name === 'VTIMEZONE') { if ('VTIMEZONE' === $array[$key]->name) {
$score = 300000000; $score = 300000000;
return $score + $key; return $score + $key;
} else { } else {
$score = 400000000; $score = 400000000;
return $score + $key; return $score + $key;
} }
} else { } else {
// Properties get encoded first // Properties get encoded first
// VCARD version 4.0 wants the VERSION property to appear first // VCARD version 4.0 wants the VERSION property to appear first
if ($array[$key] instanceof Property) { if ($array[$key] instanceof Property) {
if ($array[$key]->name === 'VERSION') { if ('VERSION' === $array[$key]->name) {
$score = 100000000; $score = 100000000;
return $score + $key; return $score + $key;
} else { } else {
// All other properties // All other properties
$score = 200000000; $score = 200000000;
return $score + $key; return $score + $key;
} }
} }
} }
}; };
$tmp = $vObj->children; $children = $vObj->children();
uksort($vObj->children, function($a, $b) use ($sortScore, $tmp) { $tmp = $children;
uksort(
$children,
function ($a, $b) use ($sortScore, $tmp) {
$sA = $sortScore($a, $tmp); $sA = $sortScore($a, $tmp);
$sB = $sortScore($b, $tmp); $sB = $sortScore($b, $tmp);
return $sA - $sB; return $sA - $sB;
}
);
}); foreach ($children as $child) {
foreach($vObj->children as $child) {
if ($child instanceof Component) { if ($child instanceof Component) {
$this->serializeComponent($child); $this->serializeComponent($child);
} else { } else {
@ -572,39 +567,29 @@ HELP
$this->cWrite('cyan', 'END'); $this->cWrite('cyan', 'END');
$this->cWrite('red', ':'); $this->cWrite('red', ':');
$this->cWrite('yellow', $vObj->name."\n"); $this->cWrite('yellow', $vObj->name."\n");
} }
/** /**
* Colorizes a property. * Colorizes a property.
*
* @param Property $property
* @return void
*/ */
protected function serializeProperty(Property $property) { protected function serializeProperty(Property $property)
{
if ($property->group) { if ($property->group) {
$this->cWrite('default', $property->group); $this->cWrite('default', $property->group);
$this->cWrite('red', '.'); $this->cWrite('red', '.');
} }
$str = '';
$this->cWrite('yellow', $property->name); $this->cWrite('yellow', $property->name);
foreach ($property->parameters as $param) { foreach ($property->parameters as $param) {
$this->cWrite('red', ';'); $this->cWrite('red', ';');
$this->cWrite('blue', $param->serialize()); $this->cWrite('blue', $param->serialize());
} }
$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($property->getValue()).' bytes)');
} else { } else {
$parts = $property->getParts(); $parts = $property->getParts();
$first1 = true; $first1 = true;
// Looping through property values // Looping through property values
@ -624,42 +609,41 @@ HELP
$this->cWrite('red', ','); $this->cWrite('red', ',');
} }
$subPart = strtr($subPart, array( $subPart = strtr(
$subPart,
[
'\\' => $this->colorize('purple', '\\\\', 'green'), '\\' => $this->colorize('purple', '\\\\', 'green'),
';' => $this->colorize('purple', '\;', 'green'), ';' => $this->colorize('purple', '\;', 'green'),
',' => $this->colorize('purple', '\,', 'green'), ',' => $this->colorize('purple', '\,', 'green'),
"\n" => $this->colorize('purple', "\\n\n\t", 'green'), "\n" => $this->colorize('purple', "\\n\n\t", 'green'),
"\r" => "", "\r" => '',
)); ]
);
$this->cWrite('green', $subPart); $this->cWrite('green', $subPart);
} }
} }
} }
$this->cWrite("default", "\n"); $this->cWrite('default', "\n");
} }
/** /**
* Parses the list of arguments. * Parses the list of arguments.
*
* @param array $argv
* @return void
*/ */
protected function parseArguments(array $argv) { protected function parseArguments(array $argv)
{
$positional = array(); $positional = [];
$options = array(); $options = [];
for($ii=0; $ii < count($argv); $ii++) {
for ($ii = 0; $ii < count($argv); ++$ii) {
// Skipping the first argument. // Skipping the first argument.
if ($ii===0) continue; if (0 === $ii) {
continue;
}
$v = $argv[$ii]; $v = $argv[$ii];
if (substr($v,0,2)==='--') { if ('--' === substr($v, 0, 2)) {
// This is a long-form option. // This is a long-form option.
$optionName = substr($v, 2); $optionName = substr($v, 2);
$optionValue = true; $optionValue = true;
@ -667,39 +651,34 @@ HELP
list($optionName, $optionValue) = explode('=', $optionName); list($optionName, $optionValue) = explode('=', $optionName);
} }
$options[$optionName] = $optionValue; $options[$optionName] = $optionValue;
} elseif (substr($v,0,1) === '-' && strlen($v)>1) { } elseif ('-' === substr($v, 0, 1) && strlen($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($v, 1)) as $option) {
$options[$option] = true; $options[$option] = true;
} }
} else { } else {
$positional[] = $v; $positional[] = $v;
}
} }
} return [$options, $positional];
return array($options, $positional);
} }
protected $parser; protected $parser;
/** /**
* Reads the input file * Reads the input file.
* *
* @return Component * @return Component
*/ */
protected function readInput() { protected function readInput()
{
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 ($this->inputFormat === 'mimedir') { if ('mimedir' === $this->inputFormat) {
$this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0));
} else { } else {
$this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); $this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0));
@ -707,24 +686,20 @@ HELP
} }
return $this->parser->parse(); return $this->parser->parse();
} }
/** /**
* Sends a message to STDERR. * Sends a message to STDERR.
* *
* @param string $msg * @param string $msg
* @return void
*/ */
protected function log($msg, $color = 'default') { protected function log($msg, $color = 'default')
{
if (!$this->quiet) { if (!$this->quiet) {
if ($color!=='default') { if ('default' !== $color) {
$msg = $this->colorize($color, $msg); $msg = $this->colorize($color, $msg);
} }
fwrite($this->stderr, $msg."\n"); fwrite($this->stderr, $msg."\n");
} }
} }
} }

View file

@ -2,18 +2,20 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use Sabre\Xml;
/** /**
* Component * Component.
* *
* A component represents a group of properties, such as VCALENDAR, VEVENT, or * A component represents a group of properties, such as VCALENDAR, VEVENT, or
* VCARD. * VCARD.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Component extends Node { class Component extends Node
{
/** /**
* Component name. * Component name.
* *
@ -28,7 +30,7 @@ class Component extends Node {
* *
* @var array * @var array
*/ */
public $children = array(); protected $children = [];
/** /**
* Creates a new component. * Creates a new component.
@ -41,33 +43,48 @@ class Component extends Node {
* an iCalendar object, this may be something like CALSCALE:GREGORIAN. To * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
* ensure that this does not happen, set $defaults to false. * ensure that this does not happen, set $defaults to false.
* *
* @param Document $root * @param string $name such as VCALENDAR, VEVENT
* @param string $name such as VCALENDAR, VEVENT.
* @param array $children
* @param bool $defaults * @param bool $defaults
* @return void
*/ */
public function __construct(Document $root, $name, array $children = array(), $defaults = true) { public function __construct(Document $root, $name, array $children = [], $defaults = true)
{
$this->name = strtoupper($name); $this->name = strtoupper($name);
$this->root = $root; $this->root = $root;
if ($defaults) { if ($defaults) {
$children = array_merge($this->getDefaults(), $children); // This is a terribly convoluted way to do this, but this ensures
// that the order of properties as they are specified in both
// defaults and the childrens list, are inserted in the object in a
// natural way.
$list = $this->getDefaults();
$nodes = [];
foreach ($children as $key => $value) {
if ($value instanceof Node) {
if (isset($list[$value->name])) {
unset($list[$value->name]);
} }
$nodes[] = $value;
} else {
$list[$key] = $value;
}
}
foreach ($list as $key => $value) {
$this->add($key, $value);
}
foreach ($nodes as $node) {
$this->add($node);
}
} else {
foreach ($children as $k => $child) { foreach ($children as $k => $child) {
if ($child instanceof Node) { if ($child instanceof Node) {
// Component or Property // Component or Property
$this->add($child); $this->add($child);
} else { } else {
// Property key=>value // Property key=>value
$this->add($k, $child); $this->add($k, $child);
} }
} }
}
} }
/** /**
@ -77,37 +94,36 @@ class Component extends Node {
* *
* add(Component $comp) // Adds a new component * add(Component $comp) // Adds a new component
* add(Property $prop) // Adds a new property * add(Property $prop) // Adds a new property
* add($name, $value, array $parameters = array()) // Adds a new property * add($name, $value, array $parameters = []) // Adds a new property
* add($name, array $children = array()) // Adds a new component * add($name, array $children = []) // Adds a new component
* by name. * by name.
* *
* @return Node * @return Node
*/ */
public function add($a1, $a2 = null, $a3 = null) { public function add()
{
$arguments = func_get_args();
if ($a1 instanceof Node) { if ($arguments[0] instanceof Node) {
if (!is_null($a2)) { if (isset($arguments[1])) {
throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
} }
$a1->parent = $this; $arguments[0]->parent = $this;
$this->children[] = $a1; $newNode = $arguments[0];
} elseif (is_string($arguments[0])) {
return $a1; $newNode = call_user_func_array([$this->root, 'create'], $arguments);
} elseif(is_string($a1)) {
$item = $this->root->create($a1, $a2, $a3);
$item->parent = $this;
$this->children[] = $item;
return $item;
} else { } else {
throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
} }
$name = $newNode->name;
if (isset($this->children[$name])) {
$this->children[$name][] = $newNode;
} else {
$this->children[$name] = [$newNode];
}
return $newNode;
} }
/** /**
@ -118,43 +134,51 @@ class Component extends Node {
* pass an instance of a property or component, in which case only that * pass an instance of a property or component, in which case only that
* exact item will be removed. * exact item will be removed.
* *
* The removed item will be returned. In case there were more than 1 items * @param string|Property|Component $item
* removed, only the last one will be returned.
*
* @param mixed $item
* @return void
*/ */
public function remove($item) { public function remove($item)
{
if (is_string($item)) { if (is_string($item)) {
$children = $this->select($item); // If there's no dot in the name, it's an exact property name and
foreach($children as $k=>$child) { // we can just wipe out all those properties.
unset($this->children[$k]); //
if (false === strpos($item, '.')) {
unset($this->children[strtoupper($item)]);
return;
}
// If there was a dot, we need to ask select() to help us out and
// then we just call remove recursively.
foreach ($this->select($item) as $child) {
$this->remove($child);
} }
return $child;
} else { } else {
foreach($this->children as $k => $child) { foreach ($this->select($item->name) as $k => $child) {
if ($child === $item) { if ($child === $item) {
unset($this->children[$k]); unset($this->children[$item->name][$k]);
return $child;
return;
} }
} }
throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component'); throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component');
} }
} }
/** /**
* Returns an iterable list of children * Returns a flat list of all the properties and components in this
* component.
* *
* @return array * @return array
*/ */
public function children() { public function children()
{
return $this->children; $result = [];
foreach ($this->children as $childGroup) {
$result = array_merge($result, $childGroup);
}
return $result;
} }
/** /**
@ -163,17 +187,19 @@ class Component extends Node {
* *
* @return array * @return array
*/ */
public function getComponents() { public function getComponents()
{
$result = [];
$result = array(); foreach ($this->children as $childGroup) {
foreach($this->children as $child) { foreach ($childGroup as $child) {
if ($child instanceof Component) { if ($child instanceof self) {
$result[] = $child; $result[] = $child;
} }
} }
}
return $result; return $result;
} }
/** /**
@ -186,36 +212,50 @@ class Component extends Node {
* string ("HOME.EMAIL"). If you want to search on a specific property that * string ("HOME.EMAIL"). If you want to search on a specific property that
* has not been assigned a group, specify ".EMAIL". * has not been assigned a group, specify ".EMAIL".
* *
* Keys are retained from the 'children' array, which may be confusing in
* certain cases.
*
* @param string $name * @param string $name
*
* @return array * @return array
*/ */
public function select($name) { public function select($name)
{
$group = null; $group = null;
$name = strtoupper($name); $name = strtoupper($name);
if (strpos($name,'.')!==false) { if (false !== strpos($name, '.')) {
list($group, $name) = explode('.', $name, 2); list($group, $name) = explode('.', $name, 2);
} }
if ('' === $name) {
$result = array(); $name = null;
foreach($this->children as $key=>$child) {
if (
strtoupper($child->name) === $name &&
(is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group))
) {
$result[$key] = $child;
}
} }
reset($result); if (!is_null($name)) {
$result = isset($this->children[$name]) ? $this->children[$name] : [];
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 && strtoupper($child->group) === $group;
}
);
}
}
// If we got to this point, it means there was no 'name' specified for
// searching, implying that this is a group-only search.
$result = [];
foreach ($this->children as $childGroup) {
foreach ($childGroup as $child) {
if ($child instanceof Property && strtoupper($child->group) === $group) {
$result[] = $child;
}
}
}
return $result;
} }
/** /**
@ -223,9 +263,9 @@ class Component extends Node {
* *
* @return string * @return string
*/ */
public function serialize() { public function serialize()
{
$str = "BEGIN:" . $this->name . "\r\n"; $str = 'BEGIN:'.$this->name."\r\n";
/** /**
* Gives a component a 'score' for sorting purposes. * Gives a component a 'score' for sorting purposes.
@ -234,85 +274,138 @@ class Component extends Node {
* *
* A higher score means the item will be lower in the list. * A higher score means the item will be lower in the list.
* To avoid score collisions, each "score category" has a reasonable * To avoid score collisions, each "score category" has a reasonable
* space to accomodate elements. The $key is added to the $score to * space to accommodate elements. The $key is added to the $score to
* preserve the original relative order of elements. * preserve the original relative order of elements.
* *
* @param int $key * @param int $key
* @param array $array * @param array $array
*
* @return int * @return int
*/ */
$sortScore = function ($key, $array) { $sortScore = function ($key, $array) {
if ($array[$key] instanceof Component) { if ($array[$key] instanceof Component) {
// We want to encode VTIMEZONE first, this is a personal // We want to encode VTIMEZONE first, this is a personal
// preference. // preference.
if ($array[$key]->name === 'VTIMEZONE') { if ('VTIMEZONE' === $array[$key]->name) {
$score = 300000000; $score = 300000000;
return $score + $key; return $score + $key;
} else { } else {
$score = 400000000; $score = 400000000;
return $score + $key; return $score + $key;
} }
} else { } else {
// Properties get encoded first // Properties get encoded first
// VCARD version 4.0 wants the VERSION property to appear first // VCARD version 4.0 wants the VERSION property to appear first
if ($array[$key] instanceof Property) { if ($array[$key] instanceof Property) {
if ($array[$key]->name === 'VERSION') { if ('VERSION' === $array[$key]->name) {
$score = 100000000; $score = 100000000;
return $score + $key; return $score + $key;
} else { } else {
// All other properties // All other properties
$score = 200000000; $score = 200000000;
return $score + $key; return $score + $key;
} }
} }
} }
}; };
$tmp = $this->children; $children = $this->children();
uksort($this->children, function($a, $b) use ($sortScore, $tmp) { $tmp = $children;
uksort(
$children,
function ($a, $b) use ($sortScore, $tmp) {
$sA = $sortScore($a, $tmp); $sA = $sortScore($a, $tmp);
$sB = $sortScore($b, $tmp); $sB = $sortScore($b, $tmp);
return $sA - $sB; return $sA - $sB;
}
);
}); foreach ($children as $child) {
$str .= $child->serialize();
foreach($this->children as $child) $str.=$child->serialize(); }
$str.= "END:" . $this->name . "\r\n"; $str .= 'END:'.$this->name."\r\n";
return $str; return $str;
} }
/** /**
* This method returns an array, with the representation as it should be * This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents. * encoded in JSON. This is used to create jCard or jCal documents.
* *
* @return array * @return array
*/ */
public function jsonSerialize() { #[\ReturnTypeWillChange]
public function jsonSerialize()
{
$components = [];
$properties = [];
$components = array(); foreach ($this->children as $childGroup) {
$properties = array(); foreach ($childGroup as $child) {
if ($child instanceof self) {
foreach($this->children as $child) {
if ($child instanceof Component) {
$components[] = $child->jsonSerialize(); $components[] = $child->jsonSerialize();
} else { } else {
$properties[] = $child->jsonSerialize(); $properties[] = $child->jsonSerialize();
} }
} }
}
return array( return [
strtolower($this->name), strtolower($this->name),
$properties, $properties,
$components $components,
); ];
}
/**
* This method serializes the data into XML. This is used to create xCard or
* xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
public function xmlSerialize(Xml\Writer $writer)
{
$components = [];
$properties = [];
foreach ($this->children as $childGroup) {
foreach ($childGroup as $child) {
if ($child instanceof self) {
$components[] = $child;
} else {
$properties[] = $child;
}
}
}
$writer->startElement(strtolower($this->name));
if (!empty($properties)) {
$writer->startElement('properties');
foreach ($properties as $property) {
$property->xmlSerialize($writer);
}
$writer->endElement();
}
if (!empty($components)) {
$writer->startElement('components');
foreach ($components as $component) {
$component->xmlSerialize($writer);
}
$writer->endElement();
}
$writer->endElement();
} }
/** /**
@ -320,10 +413,9 @@ class Component extends Node {
* *
* @return array * @return array
*/ */
protected function getDefaults() { protected function getDefaults()
{
return array(); return [];
} }
/* Magic property accessors {{{ */ /* Magic property accessors {{{ */
@ -339,37 +431,43 @@ class Component extends Node {
* $event = $calendar->VEVENT; * $event = $calendar->VEVENT;
* *
* @param string $name * @param string $name
* @return Property *
* @return Property|null
*/ */
public function __get($name) { public function __get($name)
{
$matches = $this->select($name); if ('children' === $name) {
if (count($matches)===0) { throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead');
return null;
} else {
$firstMatch = current($matches);
/** @var $firstMatch Property */
$firstMatch->setIterator(new ElementList(array_values($matches)));
return $firstMatch;
} }
$matches = $this->select($name);
if (0 === count($matches)) {
return;
} else {
$firstMatch = current($matches);
/* @var $firstMatch Property */
$firstMatch->setIterator(new ElementList(array_values($matches)));
return $firstMatch;
}
} }
/** /**
* This method checks if a sub-element with the specified name exists. * This method checks if a sub-element with the specified name exists.
* *
* @param string $name * @param string $name
*
* @return bool * @return bool
*/ */
public function __isset($name) { public function __isset($name)
{
$matches = $this->select($name); $matches = $this->select($name);
return count($matches)>0;
return count($matches) > 0;
} }
/** /**
* Using the setter method you can add properties or subcomponents * Using the setter method you can add properties or subcomponents.
* *
* You can either pass a Component, Property * You can either pass a Component, Property
* object, or a string to automatically create a Property. * object, or a string to automatically create a Property.
@ -379,32 +477,16 @@ class Component extends Node {
* *
* @param string $name * @param string $name
* @param mixed $value * @param mixed $value
* @return void
*/ */
public function __set($name, $value) { public function __set($name, $value)
{
$matches = $this->select($name); $name = strtoupper($name);
$overWrite = count($matches)?key($matches):null; $this->remove($name);
if ($value instanceof self || $value instanceof Property) {
if ($value instanceof Component || $value instanceof Property) { $this->add($value);
$value->parent = $this;
if (!is_null($overWrite)) {
$this->children[$overWrite] = $value;
} else { } else {
$this->children[] = $value; $this->add($name, $value);
} }
} elseif (is_scalar($value) || is_array($value) || is_null($value)) {
$property = $this->root->create($name,$value);
$property->parent = $this;
if (!is_null($overWrite)) {
$this->children[$overWrite] = $property;
} else {
$this->children[] = $property;
}
} else {
throw new \InvalidArgumentException('You must pass a \\Sabre\\VObject\\Component, \\Sabre\\VObject\\Property or scalar type');
}
} }
/** /**
@ -412,18 +494,10 @@ class Component extends Node {
* specified name. * specified name.
* *
* @param string $name * @param string $name
* @return void
*/ */
public function __unset($name) { public function __unset($name)
{
$matches = $this->select($name); $this->remove($name);
foreach($matches as $k=>$child) {
unset($this->children[$k]);
$child->parent = null;
}
} }
/* }}} */ /* }}} */
@ -431,43 +505,168 @@ class Component extends Node {
/** /**
* This method is automatically called when the object is cloned. * This method is automatically called when the object is cloned.
* Specifically, this will ensure all child elements are also cloned. * Specifically, this will ensure all child elements are also cloned.
*
* @return void
*/ */
public function __clone() { public function __clone()
{
foreach($this->children as $key=>$child) { foreach ($this->children as $childName => $childGroup) {
$this->children[$key] = clone $child; foreach ($childGroup as $key => $child) {
$this->children[$key]->parent = $this; $clonedChild = clone $child;
$clonedChild->parent = $this;
$clonedChild->root = $this->root;
$this->children[$childName][$key] = $clonedChild;
}
}
} }
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* It is also possible to specify defaults and severity levels for
* violating the rule.
*
* See the VEVENT implementation for getValidationRules for a more complex
* example.
*
* @var array
*/
public function getValidationRules()
{
return [];
} }
/** /**
* Validates the node for correctness. * Validates the node for correctness.
* *
* The following options are supported: * The following options are supported:
* - Node::REPAIR - If something is broken, an automatic repair may * Node::REPAIR - May attempt to automatically repair the problem.
* be attempted. * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
* *
* An array is returned with warnings. * This method returns an array with detected problems.
* Every element has the following properties:
* *
* Every item in the array has the following properties: * * level - problem level.
* * level - (number between 1 and 3 with severity information) * * message - A human-readable string describing the issue.
* * message - (human readable message) * * node - A reference to the problematic node.
* * node - (reference to the offending node) *
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on).
* 2 - A warning.
* 3 - An error.
* *
* @param int $options * @param int $options
*
* @return array * @return array
*/ */
public function validate($options = 0) { public function validate($options = 0)
{
$rules = $this->getValidationRules();
$defaults = $this->getDefaults();
$result = array(); $propertyCounters = [];
foreach($this->children as $child) {
$result = array_merge($result, $child->validate($options)); $messages = [];
foreach ($this->children() as $child) {
$name = strtoupper($child->name);
if (!isset($propertyCounters[$name])) {
$propertyCounters[$name] = 1;
} else {
++$propertyCounters[$name];
} }
return $result; $messages = array_merge($messages, $child->validate($options));
} }
foreach ($rules as $propName => $rule) {
switch ($rule) {
case '0':
if (isset($propertyCounters[$propName])) {
$messages[] = [
'level' => 3,
'message' => $propName.' MUST NOT appear in a '.$this->name.' component',
'node' => $this,
];
}
break;
case '1':
if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) {
$repaired = false;
if ($options & self::REPAIR && isset($defaults[$propName])) {
$this->add($propName, $defaults[$propName]);
$repaired = true;
}
$messages[] = [
'level' => $repaired ? 1 : 3,
'message' => $propName.' MUST appear exactly once in a '.$this->name.' component',
'node' => $this,
];
}
break;
case '+':
if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) {
$messages[] = [
'level' => 3,
'message' => $propName.' MUST appear at least once in a '.$this->name.' component',
'node' => $this,
];
}
break;
case '*':
break;
case '?':
if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) {
$level = 3;
// We try to repair the same property appearing multiple times with the exact same value
// by removing the duplicates and keeping only one property
if ($options & self::REPAIR) {
$properties = array_unique($this->select($propName), SORT_REGULAR);
if (1 === count($properties)) {
$this->remove($propName);
$this->add($properties[0]);
$level = 1;
}
}
$messages[] = [
'level' => $level,
'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component',
'node' => $this,
];
}
break;
}
}
return $messages;
}
/**
* Call this method on a document if you're done using it.
*
* It's intended to remove all circular references, so PHP can easily clean
* it up.
*/
public function destroy()
{
parent::destroy();
foreach ($this->children as $childGroup) {
foreach ($childGroup as $child) {
$child->destroy();
}
}
$this->children = [];
}
} }

View file

@ -0,0 +1,123 @@
<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
/**
* The Available sub-component.
*
* This component adds functionality to a component, specific for AVAILABLE
* components.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Ivan Enderlin
* @license http://sabre.io/license/ Modified BSD License
*/
class Available extends VObject\Component
{
/**
* Returns the 'effective start' and 'effective end' of this VAVAILABILITY
* component.
*
* We use the DTSTART and DTEND or DURATION to determine this.
*
* The returned value is an array containing DateTimeImmutable instances.
* If either the start or end is 'unbounded' its value will be null
* instead.
*
* @return array
*/
public function getEffectiveStartEnd()
{
$effectiveStart = $this->DTSTART->getDateTime();
if (isset($this->DTEND)) {
$effectiveEnd = $this->DTEND->getDateTime();
} else {
$effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
}
return [$effectiveStart, $effectiveEnd];
}
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'UID' => 1,
'DTSTART' => 1,
'DTSTAMP' => 1,
'DTEND' => '?',
'DURATION' => '?',
'CREATED' => '?',
'DESCRIPTION' => '?',
'LAST-MODIFIED' => '?',
'RECURRENCE-ID' => '?',
'RRULE' => '?',
'SUMMARY' => '?',
'CATEGORIES' => '*',
'COMMENT' => '*',
'CONTACT' => '*',
'EXDATE' => '*',
'RDATE' => '*',
'AVAILABLE' => '*',
];
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* Node::REPAIR - May attempt to automatically repair the problem.
* Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on).
* 2 - A warning.
* 3 - An error.
*
* @param int $options
*
* @return array
*/
public function validate($options = 0)
{
$result = parent::validate($options);
if (isset($this->DTEND) && isset($this->DURATION)) {
$result[] = [
'level' => 3,
'message' => 'DTEND and DURATION cannot both be present',
'node' => $this,
];
}
return $result;
}
}

View file

@ -1,71 +1,74 @@
<?php <?php
namespace Sabre\VObject\Component; namespace Sabre\VObject\Component;
use DateTimeImmutable;
use DateTimeInterface;
use Sabre\VObject; use Sabre\VObject;
use Sabre\VObject\InvalidDataException;
/** /**
* VAlarm component * VAlarm component.
* *
* This component contains some additional functionality specific for VALARMs. * This component contains some additional functionality specific for VALARMs.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VAlarm extends VObject\Component { class VAlarm extends VObject\Component
{
/** /**
* Returns a DateTime object when this alarm is going to trigger. * Returns a DateTime object when this alarm is going to trigger.
* *
* This ignores repeated alarm, only the first trigger is returned. * This ignores repeated alarm, only the first trigger is returned.
* *
* @return DateTime * @return DateTimeImmutable
*/ */
public function getEffectiveTriggerTime() { public function getEffectiveTriggerTime()
{
$trigger = $this->TRIGGER; $trigger = $this->TRIGGER;
if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { if (!isset($trigger['VALUE']) || 'DURATION' === strtoupper($trigger['VALUE'])) {
$triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
$related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; $related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START';
$parentComponent = $this->parent; $parentComponent = $this->parent;
if ($related === 'START') { if ('START' === $related) {
if ('VTODO' === $parentComponent->name) {
if ($parentComponent->name === 'VTODO') {
$propName = 'DUE'; $propName = 'DUE';
} else { } else {
$propName = 'DTSTART'; $propName = 'DTSTART';
} }
$effectiveTrigger = clone $parentComponent->$propName->getDateTime(); $effectiveTrigger = $parentComponent->$propName->getDateTime();
$effectiveTrigger->add($triggerDuration); $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
} else { } else {
if ($parentComponent->name === 'VTODO') { if ('VTODO' === $parentComponent->name) {
$endProp = 'DUE'; $endProp = 'DUE';
} elseif ($parentComponent->name === 'VEVENT') { } elseif ('VEVENT' === $parentComponent->name) {
$endProp = 'DTEND'; $endProp = 'DTEND';
} else { } else {
throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT');
} }
if (isset($parentComponent->$endProp)) { if (isset($parentComponent->$endProp)) {
$effectiveTrigger = clone $parentComponent->$endProp->getDateTime(); $effectiveTrigger = $parentComponent->$endProp->getDateTime();
$effectiveTrigger->add($triggerDuration); $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
} elseif (isset($parentComponent->DURATION)) { } elseif (isset($parentComponent->DURATION)) {
$effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); $effectiveTrigger = $parentComponent->DTSTART->getDateTime();
$duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
$effectiveTrigger->add($duration); $effectiveTrigger = $effectiveTrigger->add($duration);
$effectiveTrigger->add($triggerDuration); $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
} else { } else {
$effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); $effectiveTrigger = $parentComponent->DTSTART->getDateTime();
$effectiveTrigger->add($triggerDuration); $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
} }
} }
} else { } else {
$effectiveTrigger = $trigger->getDateTime(); $effectiveTrigger = $trigger->getDateTime();
} }
return $effectiveTrigger;
return $effectiveTrigger;
} }
/** /**
@ -75,17 +78,18 @@ class VAlarm extends VObject\Component {
* The rules used to determine if an event falls within the specified * The rules used to determine if an event falls within the specified
* time-range is based on the CalDAV specification. * time-range is based on the CalDAV specification.
* *
* @param \DateTime $start * @param DateTime $start
* @param \DateTime $end * @param DateTime $end
*
* @return bool * @return bool
*/ */
public function isInTimeRange(\DateTime $start, \DateTime $end) { public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
{
$effectiveTrigger = $this->getEffectiveTriggerTime(); $effectiveTrigger = $this->getEffectiveTriggerTime();
if (isset($this->DURATION)) { if (isset($this->DURATION)) {
$duration = VObject\DateTimeParser::parseDuration($this->DURATION); $duration = VObject\DateTimeParser::parseDuration($this->DURATION);
$repeat = (string)$this->repeat; $repeat = (string) $this->REPEAT;
if (!$repeat) { if (!$repeat) {
$repeat = 1; $repeat = 1;
} }
@ -93,16 +97,42 @@ class VAlarm extends VObject\Component {
$period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat); $period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat);
foreach ($period as $occurrence) { foreach ($period as $occurrence) {
if ($start <= $occurrence && $end > $occurrence) { if ($start <= $occurrence && $end > $occurrence) {
return true; return true;
} }
} }
return false; return false;
} else { } else {
return ($start <= $effectiveTrigger && $end > $effectiveTrigger); return $start <= $effectiveTrigger && $end > $effectiveTrigger;
}
} }
} /**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'ACTION' => 1,
'TRIGGER' => 1,
'DURATION' => '?',
'REPEAT' => '?',
'ATTACH' => '?',
];
}
} }

View file

@ -0,0 +1,149 @@
<?php
namespace Sabre\VObject\Component;
use DateTimeInterface;
use Sabre\VObject;
/**
* The VAvailability component.
*
* This component adds functionality to a component, specific for VAVAILABILITY
* components.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Ivan Enderlin
* @license http://sabre.io/license/ Modified BSD License
*/
class VAvailability extends VObject\Component
{
/**
* Returns true or false depending on if the event falls in the specified
* time-range. This is used for filtering purposes.
*
* The rules used to determine if an event falls within the specified
* time-range is based on:
*
* https://tools.ietf.org/html/draft-daboo-calendar-availability-05#section-3.1
*
* @return bool
*/
public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
{
list($effectiveStart, $effectiveEnd) = $this->getEffectiveStartEnd();
return
(is_null($effectiveStart) || $start < $effectiveEnd) &&
(is_null($effectiveEnd) || $end > $effectiveStart)
;
}
/**
* Returns the 'effective start' and 'effective end' of this VAVAILABILITY
* component.
*
* We use the DTSTART and DTEND or DURATION to determine this.
*
* The returned value is an array containing DateTimeImmutable instances.
* If either the start or end is 'unbounded' its value will be null
* instead.
*
* @return array
*/
public function getEffectiveStartEnd()
{
$effectiveStart = null;
$effectiveEnd = null;
if (isset($this->DTSTART)) {
$effectiveStart = $this->DTSTART->getDateTime();
}
if (isset($this->DTEND)) {
$effectiveEnd = $this->DTEND->getDateTime();
} elseif ($effectiveStart && isset($this->DURATION)) {
$effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
}
return [$effectiveStart, $effectiveEnd];
}
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'UID' => 1,
'DTSTAMP' => 1,
'BUSYTYPE' => '?',
'CLASS' => '?',
'CREATED' => '?',
'DESCRIPTION' => '?',
'DTSTART' => '?',
'LAST-MODIFIED' => '?',
'ORGANIZER' => '?',
'PRIORITY' => '?',
'SEQUENCE' => '?',
'SUMMARY' => '?',
'URL' => '?',
'DTEND' => '?',
'DURATION' => '?',
'CATEGORIES' => '*',
'COMMENT' => '*',
'CONTACT' => '*',
];
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* Node::REPAIR - May attempt to automatically repair the problem.
* Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on).
* 2 - A warning.
* 3 - An error.
*
* @param int $options
*
* @return array
*/
public function validate($options = 0)
{
$result = parent::validate($options);
if (isset($this->DTEND) && isset($this->DURATION)) {
$result[] = [
'level' => 3,
'message' => 'DTEND and DURATION cannot both be present',
'node' => $this,
];
}
return $result;
}
}

View file

@ -2,21 +2,26 @@
namespace Sabre\VObject\Component; namespace Sabre\VObject\Component;
use use DateTimeInterface;
Sabre\VObject, use DateTimeZone;
Sabre\VObject\Component; use Sabre\VObject;
use Sabre\VObject\Component;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\Property;
use Sabre\VObject\Recur\EventIterator;
use Sabre\VObject\Recur\NoInstancesException;
/** /**
* The VCalendar component * The VCalendar component.
* *
* This component adds functionality to a component, specific for a VCALENDAR. * This component adds functionality to a component, specific for a VCALENDAR.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VCalendar extends VObject\Document { class VCalendar extends VObject\Document
{
/** /**
* The default name for this component. * The default name for this component.
* *
@ -24,132 +29,137 @@ class VCalendar extends VObject\Document {
* *
* @var string * @var string
*/ */
static public $defaultName = 'VCALENDAR'; public static $defaultName = 'VCALENDAR';
/** /**
* This is a list of components, and which classes they should map to. * This is a list of components, and which classes they should map to.
* *
* @var array * @var array
*/ */
static public $componentMap = array( public static $componentMap = [
'VEVENT' => 'Sabre\\VObject\\Component\\VEvent', 'VCALENDAR' => self::class,
'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy', 'VALARM' => VAlarm::class,
'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', 'VEVENT' => VEvent::class,
'VTODO' => 'Sabre\\VObject\\Component\\VTodo', 'VFREEBUSY' => VFreeBusy::class,
'VALARM' => 'Sabre\\VObject\\Component\\VAlarm', 'VAVAILABILITY' => VAvailability::class,
); 'AVAILABLE' => Available::class,
'VJOURNAL' => VJournal::class,
'VTIMEZONE' => VTimeZone::class,
'VTODO' => VTodo::class,
];
/** /**
* List of value-types, and which classes they map to. * List of value-types, and which classes they map to.
* *
* @var array * @var array
*/ */
static public $valueMap = array( public static $valueMap = [
'BINARY' => 'Sabre\\VObject\\Property\\Binary', 'BINARY' => VObject\Property\Binary::class,
'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', 'BOOLEAN' => VObject\Property\Boolean::class,
'CAL-ADDRESS' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class,
'DATE' => 'Sabre\\VObject\\Property\\ICalendar\\Date', 'DATE' => VObject\Property\ICalendar\Date::class,
'DATE-TIME' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class,
'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', 'DURATION' => VObject\Property\ICalendar\Duration::class,
'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue', 'FLOAT' => VObject\Property\FloatValue::class,
'INTEGER' => 'Sabre\\VObject\\Property\\Integer', 'INTEGER' => VObject\Property\IntegerValue::class,
'PERIOD' => 'Sabre\\VObject\\Property\\ICalendar\\Period', 'PERIOD' => VObject\Property\ICalendar\Period::class,
'RECUR' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', 'RECUR' => VObject\Property\ICalendar\Recur::class,
'TEXT' => 'Sabre\\VObject\\Property\\Text', 'TEXT' => VObject\Property\Text::class,
'TIME' => 'Sabre\\VObject\\Property\\Time', 'TIME' => VObject\Property\Time::class,
'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
'URI' => 'Sabre\\VObject\\Property\\Uri', 'URI' => VObject\Property\Uri::class,
'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', 'UTC-OFFSET' => VObject\Property\UtcOffset::class,
); ];
/** /**
* List of properties, and which classes they map to. * List of properties, and which classes they map to.
* *
* @var array * @var array
*/ */
static public $propertyMap = array( public static $propertyMap = [
// Calendar properties // Calendar properties
'CALSCALE' => 'Sabre\\VObject\\Property\\FlatText', 'CALSCALE' => VObject\Property\FlatText::class,
'METHOD' => 'Sabre\\VObject\\Property\\FlatText', 'METHOD' => VObject\Property\FlatText::class,
'PRODID' => 'Sabre\\VObject\\Property\\FlatText', 'PRODID' => VObject\Property\FlatText::class,
'VERSION' => 'Sabre\\VObject\\Property\\FlatText', 'VERSION' => VObject\Property\FlatText::class,
// Component properties // Component properties
'ATTACH' => 'Sabre\\VObject\\Property\\Binary', 'ATTACH' => VObject\Property\Uri::class,
'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', 'CATEGORIES' => VObject\Property\Text::class,
'CLASS' => 'Sabre\\VObject\\Property\\FlatText', 'CLASS' => VObject\Property\FlatText::class,
'COMMENT' => 'Sabre\\VObject\\Property\\FlatText', 'COMMENT' => VObject\Property\FlatText::class,
'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText', 'DESCRIPTION' => VObject\Property\FlatText::class,
'GEO' => 'Sabre\\VObject\\Property\\FloatValue', 'GEO' => VObject\Property\FloatValue::class,
'LOCATION' => 'Sabre\\VObject\\Property\\FlatText', 'LOCATION' => VObject\Property\FlatText::class,
'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\Integer', 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class,
'PRIORITY' => 'Sabre\\VObject\\Property\\Integer', 'PRIORITY' => VObject\Property\IntegerValue::class,
'RESOURCES' => 'Sabre\\VObject\\Property\\Text', 'RESOURCES' => VObject\Property\Text::class,
'STATUS' => 'Sabre\\VObject\\Property\\FlatText', 'STATUS' => VObject\Property\FlatText::class,
'SUMMARY' => 'Sabre\\VObject\\Property\\FlatText', 'SUMMARY' => VObject\Property\FlatText::class,
// Date and Time Component Properties // Date and Time Component Properties
'COMPLETED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'COMPLETED' => VObject\Property\ICalendar\DateTime::class,
'DTEND' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'DTEND' => VObject\Property\ICalendar\DateTime::class,
'DUE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'DUE' => VObject\Property\ICalendar\DateTime::class,
'DTSTART' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'DTSTART' => VObject\Property\ICalendar\DateTime::class,
'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', 'DURATION' => VObject\Property\ICalendar\Duration::class,
'FREEBUSY' => 'Sabre\\VObject\\Property\\ICalendar\\Period', 'FREEBUSY' => VObject\Property\ICalendar\Period::class,
'TRANSP' => 'Sabre\\VObject\\Property\\FlatText', 'TRANSP' => VObject\Property\FlatText::class,
// Time Zone Component Properties // Time Zone Component Properties
'TZID' => 'Sabre\\VObject\\Property\\FlatText', 'TZID' => VObject\Property\FlatText::class,
'TZNAME' => 'Sabre\\VObject\\Property\\FlatText', 'TZNAME' => VObject\Property\FlatText::class,
'TZOFFSETFROM' => 'Sabre\\VObject\\Property\\UtcOffset', 'TZOFFSETFROM' => VObject\Property\UtcOffset::class,
'TZOFFSETTO' => 'Sabre\\VObject\\Property\\UtcOffset', 'TZOFFSETTO' => VObject\Property\UtcOffset::class,
'TZURL' => 'Sabre\\VObject\\Property\\Uri', 'TZURL' => VObject\Property\Uri::class,
// Relationship Component Properties // Relationship Component Properties
'ATTENDEE' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class,
'CONTACT' => 'Sabre\\VObject\\Property\\FlatText', 'CONTACT' => VObject\Property\FlatText::class,
'ORGANIZER' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class,
'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class,
'RELATED-TO' => 'Sabre\\VObject\\Property\\FlatText', 'RELATED-TO' => VObject\Property\FlatText::class,
'URL' => 'Sabre\\VObject\\Property\\Uri', 'URL' => VObject\Property\Uri::class,
'UID' => 'Sabre\\VObject\\Property\\FlatText', 'UID' => VObject\Property\FlatText::class,
// Recurrence Component Properties // Recurrence Component Properties
'EXDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'EXDATE' => VObject\Property\ICalendar\DateTime::class,
'RDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'RDATE' => VObject\Property\ICalendar\DateTime::class,
'RRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', 'RRULE' => VObject\Property\ICalendar\Recur::class,
'EXRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', // Deprecated since rfc5545 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545
// Alarm Component Properties // Alarm Component Properties
'ACTION' => 'Sabre\\VObject\\Property\\FlatText', 'ACTION' => VObject\Property\FlatText::class,
'REPEAT' => 'Sabre\\VObject\\Property\\Integer', 'REPEAT' => VObject\Property\IntegerValue::class,
'TRIGGER' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', 'TRIGGER' => VObject\Property\ICalendar\Duration::class,
// Change Management Component Properties // Change Management Component Properties
'CREATED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'CREATED' => VObject\Property\ICalendar\DateTime::class,
'DTSTAMP' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class,
'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class,
'SEQUENCE' => 'Sabre\\VObject\\Property\\Integer', 'SEQUENCE' => VObject\Property\IntegerValue::class,
// Request Status // Request Status
'REQUEST-STATUS' => 'Sabre\\VObject\\Property\\Text', 'REQUEST-STATUS' => VObject\Property\Text::class,
// Additions from draft-daboo-valarm-extensions-04 // Additions from draft-daboo-valarm-extensions-04
'ALARM-AGENT' => 'Sabre\\VObject\\Property\\Text', 'ALARM-AGENT' => VObject\Property\Text::class,
'ACKNOWLEDGED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class,
'PROXIMITY' => 'Sabre\\VObject\\Property\\Text', 'PROXIMITY' => VObject\Property\Text::class,
'DEFAULT-ALARM' => 'Sabre\\VObject\\Property\\Boolean', 'DEFAULT-ALARM' => VObject\Property\Boolean::class,
); // Additions from draft-daboo-calendar-availability-05
'BUSYTYPE' => VObject\Property\Text::class,
];
/** /**
* Returns the current document type. * Returns the current document type.
* *
* @return void * @return int
*/ */
public function getDocumentType() { public function getDocumentType()
{
return self::ICALENDAR20; return self::ICALENDAR20;
} }
/** /**
@ -160,118 +170,188 @@ class VCalendar extends VObject\Document {
* VTIMEZONE components will always be excluded. * VTIMEZONE components will always be excluded.
* *
* @param string $componentName filter by component name * @param string $componentName filter by component name
* @return array *
* @return VObject\Component[]
*/ */
public function getBaseComponents($componentName = null) { public function getBaseComponents($componentName = null)
{
$isBaseComponent = function ($component) {
if (!$component instanceof VObject\Component) {
return false;
}
if ('VTIMEZONE' === $component->name) {
return false;
}
if (isset($component->{'RECURRENCE-ID'})) {
return false;
}
$components = array(); return true;
foreach($this->children as $component) { };
if (!$component instanceof VObject\Component) if ($componentName) {
continue; // Early exit
return array_filter(
if (isset($component->{'RECURRENCE-ID'})) $this->select($componentName),
continue; $isBaseComponent
);
if ($componentName && $component->name !== strtoupper($componentName)) }
continue;
if ($component->name === 'VTIMEZONE')
continue;
$components[] = $component;
$components = [];
foreach ($this->children as $childGroup) {
foreach ($childGroup as $child) {
if (!$child instanceof Component) {
// If one child is not a component, they all are so we skip
// the entire group.
continue 2;
}
if ($isBaseComponent($child)) {
$components[] = $child;
}
}
} }
return $components; return $components;
} }
/** /**
* Returns the first component that is not a VTIMEZONE, and does not have
* an RECURRENCE-ID.
*
* If there is no such component, null will be returned.
*
* @param string $componentName filter by component name
*
* @return VObject\Component|null
*/
public function getBaseComponent($componentName = null)
{
$isBaseComponent = function ($component) {
if (!$component instanceof VObject\Component) {
return false;
}
if ('VTIMEZONE' === $component->name) {
return false;
}
if (isset($component->{'RECURRENCE-ID'})) {
return false;
}
return true;
};
if ($componentName) {
foreach ($this->select($componentName) as $child) {
if ($isBaseComponent($child)) {
return $child;
}
}
return null;
}
// Searching all components
foreach ($this->children as $childGroup) {
foreach ($childGroup as $child) {
if ($isBaseComponent($child)) {
return $child;
}
}
}
return null;
}
/**
* Expand all events in this VCalendar object and return a new VCalendar
* with the expanded events.
*
* If this calendar object, has events with recurrence rules, this method * If this calendar object, has events with recurrence rules, this method
* can be used to expand the event into multiple sub-events. * can be used to expand the event into multiple sub-events.
* *
* Each event will be stripped from it's recurrence information, and only * Each event will be stripped from its recurrence information, and only
* the instances of the event in the specified timerange will be left * the instances of the event in the specified timerange will be left
* alone. * alone.
* *
* In addition, this method will cause timezone information to be stripped, * In addition, this method will cause timezone information to be stripped,
* and normalized to UTC. * and normalized to UTC.
* *
* This method will alter the VCalendar. This cannot be reversed. * @param DateTimeZone $timeZone reference timezone for floating dates and
* times
* *
* This functionality is specifically used by the CalDAV standard. It is * @return VCalendar
* possible for clients to request expand events, if they are rather simple
* clients and do not have the possibility to calculate recurrences.
*
* @param DateTime $start
* @param DateTime $end
* @return void
*/ */
public function expand(\DateTime $start, \DateTime $end) { public function expand(DateTimeInterface $start, DateTimeInterface $end, DateTimeZone $timeZone = null)
{
$newChildren = [];
$recurringEvents = [];
$newEvents = array(); if (!$timeZone) {
$timeZone = new DateTimeZone('UTC');
foreach($this->select('VEVENT') as $key=>$vevent) {
if (isset($vevent->{'RECURRENCE-ID'})) {
unset($this->children[$key]);
continue;
} }
$stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones) {
if (!$vevent->rrule) { foreach ($component->children() as $componentChild) {
unset($this->children[$key]); if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) {
if ($vevent->isInTimeRange($start, $end)) { $dt = $componentChild->getDateTimes($timeZone);
$newEvents[] = $vevent;
}
continue;
}
$uid = (string)$vevent->uid;
if (!$uid) {
throw new \LogicException('Event did not have a UID!');
}
$it = new VObject\RecurrenceIterator($this, $vevent->uid);
$it->fastForward($start);
while($it->valid() && $it->getDTStart() < $end) {
if ($it->getDTEnd() > $start) {
$newEvents[] = $it->getEventObject();
}
$it->next();
}
unset($this->children[$key]);
}
// Setting all properties to UTC time.
foreach($newEvents as $newEvent) {
foreach($newEvent->children as $child) {
if ($child instanceof VObject\Property\ICalendar\DateTime && $child->hasTime()) {
$dt = $child->getDateTimes();
// We only need to update the first timezone, because // We only need to update the first timezone, because
// setDateTimes will match all other timezones to the // setDateTimes will match all other timezones to the
// first. // first.
$dt[0]->setTimeZone(new \DateTimeZone('UTC')); $dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC'));
$child->setDateTimes($dt); $componentChild->setDateTimes($dt);
} elseif ($componentChild instanceof Component) {
$stripTimezones($componentChild);
}
} }
return $component;
};
foreach ($this->children() as $child) {
if ($child instanceof Property && 'PRODID' !== $child->name) {
// We explicitly want to ignore PRODID, because we want to
// overwrite it with our own.
$newChildren[] = clone $child;
} elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) {
// We're also stripping all VTIMEZONE objects because we're
// converting everything to UTC.
if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) {
// Handle these a bit later.
$uid = (string) $child->UID;
if (!$uid) {
throw new InvalidDataException('Every VEVENT object must have a UID property');
}
if (isset($recurringEvents[$uid])) {
$recurringEvents[$uid][] = clone $child;
} else {
$recurringEvents[$uid] = [clone $child];
}
} elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) {
$newChildren[] = $stripTimezones(clone $child);
}
}
} }
$this->add($newEvent); foreach ($recurringEvents as $events) {
try {
$it = new EventIterator($events, null, $timeZone);
} catch (NoInstancesException $e) {
// This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output
// entirely.
continue;
}
$it->fastForward($start);
while ($it->valid() && $it->getDTStart() < $end) {
if ($it->getDTEnd() > $start) {
$newChildren[] = $stripTimezones($it->getEventObject());
}
$it->next();
}
} }
// Removing all VTIMEZONE components return new self($newChildren);
unset($this->VTIMEZONE);
} }
/** /**
@ -279,91 +359,170 @@ class VCalendar extends VObject\Document {
* *
* @return array * @return array
*/ */
protected function getDefaults() { protected function getDefaults()
{
return array( return [
'VERSION' => '2.0', 'VERSION' => '2.0',
'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
'CALSCALE' => 'GREGORIAN', 'CALSCALE' => 'GREGORIAN',
); ];
}
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'PRODID' => 1,
'VERSION' => 1,
'CALSCALE' => '?',
'METHOD' => '?',
];
} }
/** /**
* Validates the node for correctness. * Validates the node for correctness.
* An array is returned with warnings.
* *
* Every item in the array has the following properties: * The following options are supported:
* * level - (number between 1 and 3 with severity information) * Node::REPAIR - May attempt to automatically repair the problem.
* * message - (human readable message) * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
* * node - (reference to the offending node) * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on).
* 2 - A warning.
* 3 - An error.
*
* @param int $options
* *
* @return array * @return array
*/ */
public function validate($options = 0) { public function validate($options = 0)
{
$warnings = parent::validate($options);
$warnings = array(); if ($ver = $this->VERSION) {
if ('2.0' !== (string) $ver) {
$version = $this->select('VERSION'); $warnings[] = [
if (count($version)!==1) { 'level' => 3,
$warnings[] = array(
'level' => 1,
'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time',
'node' => $this,
);
} else {
if ((string)$this->VERSION !== '2.0') {
$warnings[] = array(
'level' => 1,
'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
'node' => $this, 'node' => $this,
); ];
} }
} }
$version = $this->select('PRODID');
if (count($version)!==1) {
$warnings[] = array(
'level' => 2,
'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time',
'node' => $this,
);
}
if (count($this->CALSCALE) > 1) {
$warnings[] = array(
'level' => 2,
'message' => 'The CALSCALE property must not be specified more than once.',
'node' => $this,
);
}
if (count($this->METHOD) > 1) {
$warnings[] = array(
'level' => 2,
'message' => 'The METHOD property must not be specified more than once.',
'node' => $this,
);
}
$uidList = [];
$componentsFound = 0; $componentsFound = 0;
foreach($this->children as $child) { $componentTypes = [];
foreach ($this->children() as $child) {
if ($child instanceof Component) { if ($child instanceof Component) {
$componentsFound++; ++$componentsFound;
if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) {
continue;
}
$componentTypes[] = $child->name;
$uid = (string) $child->UID;
$isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1;
if (isset($uidList[$uid])) {
++$uidList[$uid]['count'];
if ($isMaster && $uidList[$uid]['hasMaster']) {
$warnings[] = [
'level' => 3,
'message' => 'More than one master object was found for the object with UID '.$uid,
'node' => $this,
];
}
$uidList[$uid]['hasMaster'] += $isMaster;
} else {
$uidList[$uid] = [
'count' => 1,
'hasMaster' => $isMaster,
];
}
} }
} }
if ($componentsFound===0) { if (0 === $componentsFound) {
$warnings[] = array( $warnings[] = [
'level' => 1, 'level' => 3,
'message' => 'An iCalendar object must have at least 1 component.', 'message' => 'An iCalendar object must have at least 1 component.',
'node' => $this, 'node' => $this,
); ];
} }
return array_merge( if ($options & self::PROFILE_CALDAV) {
$warnings, if (count($uidList) > 1) {
parent::validate() $warnings[] = [
); 'level' => 3,
'message' => 'A calendar object on a CalDAV server may only have components with the same UID.',
'node' => $this,
];
}
if (0 === count($componentTypes)) {
$warnings[] = [
'level' => 3,
'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).',
'node' => $this,
];
}
if (count(array_unique($componentTypes)) > 1) {
$warnings[] = [
'level' => 3,
'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).',
'node' => $this,
];
} }
if (isset($this->METHOD)) {
$warnings[] = [
'level' => 3,
'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.',
'node' => $this,
];
}
} }
return $warnings;
}
/**
* Returns all components with a specific UID value.
*
* @return array
*/
public function getByUID($uid)
{
return array_filter($this->getComponents(), function ($item) use ($uid) {
if (!$itemUid = $item->select('UID')) {
return false;
}
$itemUid = current($itemUid)->getValue();
return $uid === $itemUid;
});
}
}

View file

@ -2,21 +2,21 @@
namespace Sabre\VObject\Component; namespace Sabre\VObject\Component;
use use Sabre\VObject;
Sabre\VObject; use Sabre\Xml;
/** /**
* The VCard component * The VCard component.
* *
* This component represents the BEGIN:VCARD and END:VCARD found in every * This component represents the BEGIN:VCARD and END:VCARD found in every
* vcard. * vcard.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VCard extends VObject\Document { class VCard extends VObject\Document
{
/** /**
* The default name for this component. * The default name for this component.
* *
@ -24,106 +24,130 @@ class VCard extends VObject\Document {
* *
* @var string * @var string
*/ */
static public $defaultName = 'VCARD'; public static $defaultName = 'VCARD';
/** /**
* Caching the version number * Caching the version number.
* *
* @var int * @var int
*/ */
private $version = null; private $version = null;
/**
* This is a list of components, and which classes they should map to.
*
* @var array
*/
public static $componentMap = [
'VCARD' => VCard::class,
];
/** /**
* List of value-types, and which classes they map to. * List of value-types, and which classes they map to.
* *
* @var array * @var array
*/ */
static public $valueMap = array( public static $valueMap = [
'BINARY' => 'Sabre\\VObject\\Property\\Binary', 'BINARY' => VObject\Property\Binary::class,
'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', 'BOOLEAN' => VObject\Property\Boolean::class,
'CONTENT-ID' => 'Sabre\\VObject\\Property\\FlatText', // vCard 2.1 only 'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only
'DATE' => 'Sabre\\VObject\\Property\\VCard\\Date', 'DATE' => VObject\Property\VCard\Date::class,
'DATE-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateTime', 'DATE-TIME' => VObject\Property\VCard\DateTime::class,
'DATE-AND-OR-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', // vCard only 'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only
'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue', 'FLOAT' => VObject\Property\FloatValue::class,
'INTEGER' => 'Sabre\\VObject\\Property\\Integer', 'INTEGER' => VObject\Property\IntegerValue::class,
'LANGUAGE-TAG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', 'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class,
'TIMESTAMP' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', 'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only
'TEXT' => 'Sabre\\VObject\\Property\\Text', 'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class,
'TIME' => 'Sabre\\VObject\\Property\\Time', 'TEXT' => VObject\Property\Text::class,
'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. 'TIME' => VObject\Property\Time::class,
'URI' => 'Sabre\\VObject\\Property\\Uri', 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
'URL' => 'Sabre\\VObject\\Property\\Uri', // vCard 2.1 only 'URI' => VObject\Property\Uri::class,
'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', 'URL' => VObject\Property\Uri::class, // vCard 2.1 only
); 'UTC-OFFSET' => VObject\Property\UtcOffset::class,
];
/** /**
* List of properties, and which classes they map to. * List of properties, and which classes they map to.
* *
* @var array * @var array
*/ */
static public $propertyMap = array( public static $propertyMap = [
// vCard 2.1 properties and up // vCard 2.1 properties and up
'N' => 'Sabre\\VObject\\Property\\Text', 'N' => VObject\Property\Text::class,
'FN' => 'Sabre\\VObject\\Property\\FlatText', 'FN' => VObject\Property\FlatText::class,
'PHOTO' => 'Sabre\\VObject\\Property\\Binary', // Todo: we should add a class for Binary values. 'PHOTO' => VObject\Property\Binary::class,
'BDAY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', 'BDAY' => VObject\Property\VCard\DateAndOrTime::class,
'ADR' => 'Sabre\\VObject\\Property\\Text', 'ADR' => VObject\Property\Text::class,
'LABEL' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0
'TEL' => 'Sabre\\VObject\\Property\\FlatText', 'TEL' => VObject\Property\FlatText::class,
'EMAIL' => 'Sabre\\VObject\\Property\\FlatText', 'EMAIL' => VObject\Property\FlatText::class,
'MAILER' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0
'GEO' => 'Sabre\\VObject\\Property\\FlatText', 'GEO' => VObject\Property\FlatText::class,
'TITLE' => 'Sabre\\VObject\\Property\\FlatText', 'TITLE' => VObject\Property\FlatText::class,
'ROLE' => 'Sabre\\VObject\\Property\\FlatText', 'ROLE' => VObject\Property\FlatText::class,
'LOGO' => 'Sabre\\VObject\\Property\\Binary', 'LOGO' => VObject\Property\Binary::class,
// 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so
// not supported at the moment // not supported at the moment
'ORG' => 'Sabre\\VObject\\Property\\Text', 'ORG' => VObject\Property\Text::class,
'NOTE' => 'Sabre\\VObject\\Property\\FlatText', 'NOTE' => VObject\Property\FlatText::class,
'REV' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', 'REV' => VObject\Property\VCard\TimeStamp::class,
'SOUND' => 'Sabre\\VObject\\Property\\FlatText', 'SOUND' => VObject\Property\FlatText::class,
'URL' => 'Sabre\\VObject\\Property\\Uri', 'URL' => VObject\Property\Uri::class,
'UID' => 'Sabre\\VObject\\Property\\FlatText', 'UID' => VObject\Property\FlatText::class,
'VERSION' => 'Sabre\\VObject\\Property\\FlatText', 'VERSION' => VObject\Property\FlatText::class,
'KEY' => 'Sabre\\VObject\\Property\\FlatText', 'KEY' => VObject\Property\FlatText::class,
'TZ' => 'Sabre\\VObject\\Property\\Text', 'TZ' => VObject\Property\Text::class,
// vCard 3.0 properties // vCard 3.0 properties
'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', 'CATEGORIES' => VObject\Property\Text::class,
'SORT-STRING' => 'Sabre\\VObject\\Property\\FlatText', 'SORT-STRING' => VObject\Property\FlatText::class,
'PRODID' => 'Sabre\\VObject\\Property\\FlatText', 'PRODID' => VObject\Property\FlatText::class,
'NICKNAME' => 'Sabre\\VObject\\Property\\Text', 'NICKNAME' => VObject\Property\Text::class,
'CLASS' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0
// rfc2739 properties // rfc2739 properties
'FBURL' => 'Sabre\\VObject\\Property\\Uri', 'FBURL' => VObject\Property\Uri::class,
'CAPURI' => 'Sabre\\VObject\\Property\\Uri', 'CAPURI' => VObject\Property\Uri::class,
'CALURI' => 'Sabre\\VObject\\Property\\Uri', 'CALURI' => VObject\Property\Uri::class,
'CALADRURI' => VObject\Property\Uri::class,
// rfc4770 properties // rfc4770 properties
'IMPP' => 'Sabre\\VObject\\Property\\Uri', 'IMPP' => VObject\Property\Uri::class,
// vCard 4.0 properties // vCard 4.0 properties
'XML' => 'Sabre\\VObject\\Property\\FlatText', 'SOURCE' => VObject\Property\Uri::class,
'ANNIVERSARY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', 'XML' => VObject\Property\FlatText::class,
'CLIENTPIDMAP' => 'Sabre\\VObject\\Property\\Text', 'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class,
'LANG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', 'CLIENTPIDMAP' => VObject\Property\Text::class,
'GENDER' => 'Sabre\\VObject\\Property\\Text', 'LANG' => VObject\Property\VCard\LanguageTag::class,
'KIND' => 'Sabre\\VObject\\Property\\FlatText', 'GENDER' => VObject\Property\Text::class,
'KIND' => VObject\Property\FlatText::class,
'MEMBER' => VObject\Property\Uri::class,
'RELATED' => VObject\Property\Uri::class,
); // rfc6474 properties
'BIRTHPLACE' => VObject\Property\FlatText::class,
'DEATHPLACE' => VObject\Property\FlatText::class,
'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class,
// rfc6715 properties
'EXPERTISE' => VObject\Property\FlatText::class,
'HOBBY' => VObject\Property\FlatText::class,
'INTEREST' => VObject\Property\FlatText::class,
'ORG-DIRECTORY' => VObject\Property\FlatText::class,
];
/** /**
* Returns the current document type. * Returns the current document type.
* *
* @return void * @return int
*/ */
public function getDocumentType() { public function getDocumentType()
{
if (!$this->version) { if (!$this->version) {
$version = (string) $this->VERSION; $version = (string) $this->VERSION;
switch ($version) { switch ($version) {
case '2.1': case '2.1':
$this->version = self::VCARD21; $this->version = self::VCARD21;
@ -135,14 +159,13 @@ class VCard extends VObject\Document {
$this->version = self::VCARD40; $this->version = self::VCARD40;
break; break;
default: default:
$this->version = self::UNKNOWN; // We don't want to cache the version if it's unknown,
break; // because we might get a version property in a bit.
return self::UNKNOWN;
} }
} }
return $this->version; return $this->version;
} }
/** /**
@ -156,13 +179,14 @@ class VCard extends VObject\Document {
* If input and output version are identical, a clone is returned. * If input and output version are identical, a clone is returned.
* *
* @param int $target * @param int $target
*
* @return VCard * @return VCard
*/ */
public function convert($target) { public function convert($target)
{
$converter = new VObject\VCardConverter(); $converter = new VObject\VCardConverter();
return $converter->convert($this, $target);
return $converter->convert($this, $target);
} }
/** /**
@ -172,67 +196,85 @@ class VCard extends VObject\Document {
*/ */
const DEFAULT_VERSION = self::VCARD21; const DEFAULT_VERSION = self::VCARD21;
/** /**
* Validates the node for correctness. * Validates the node for correctness.
* *
* The following options are supported: * The following options are supported:
* - Node::REPAIR - If something is broken, and automatic repair may * Node::REPAIR - May attempt to automatically repair the problem.
* be attempted.
* *
* An array is returned with warnings. * This method returns an array with detected problems.
* Every element has the following properties:
* *
* Every item in the array has the following properties: * * level - problem level.
* * level - (number between 1 and 3 with severity information) * * message - A human-readable string describing the issue.
* * message - (human readable message) * * node - A reference to the problematic node.
* * node - (reference to the offending node) *
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on)
* 2 - An inconsequential issue
* 3 - A severe issue.
* *
* @param int $options * @param int $options
*
* @return array * @return array
*/ */
public function validate($options = 0) { public function validate($options = 0)
{
$warnings = [];
$warnings = array(); $versionMap = [
$versionMap = array(
self::VCARD21 => '2.1', self::VCARD21 => '2.1',
self::VCARD30 => '3.0', self::VCARD30 => '3.0',
self::VCARD40 => '4.0', self::VCARD40 => '4.0',
); ];
$version = $this->select('VERSION'); $version = $this->select('VERSION');
if (count($version)!==1) { if (1 === count($version)) {
$warnings[] = array(
'level' => 1,
'message' => 'The VERSION property must appear in the VCARD component exactly 1 time',
'node' => $this,
);
if ($options & self::REPAIR) {
$this->VERSION = $versionMap[self::DEFAULT_VERSION];
}
} else {
$version = (string) $this->VERSION; $version = (string) $this->VERSION;
if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') { if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) {
$warnings[] = array( $warnings[] = [
'level' => 1, 'level' => 3,
'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
'node' => $this, 'node' => $this,
); ];
if ($options & self::REPAIR) { if ($options & self::REPAIR) {
$this->VERSION = $versionMap[self::DEFAULT_VERSION]; $this->VERSION = $versionMap[self::DEFAULT_VERSION];
} }
} }
if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) {
} $warnings[] = [
$fn = $this->select('FN'); 'level' => 3,
if (count($fn)!==1) { 'message' => 'CardDAV servers are not allowed to accept vCard 2.1.',
$warnings[] = array(
'level' => 1,
'message' => 'The FN property must appear in the VCARD component exactly 1 time',
'node' => $this, 'node' => $this,
); ];
if (($options & self::REPAIR) && count($fn) === 0) { }
}
$uid = $this->select('UID');
if (0 === count($uid)) {
if ($options & self::PROFILE_CARDDAV) {
// Required for CardDAV
$warningLevel = 3;
$message = 'vCards on CardDAV servers MUST have a UID property.';
} else {
// Not required for regular vcards
$warningLevel = 2;
$message = 'Adding a UID to a vCard property is recommended.';
}
if ($options & self::REPAIR) {
$this->UID = VObject\UUIDUtil::getUUID();
$warningLevel = 1;
}
$warnings[] = [
'level' => $warningLevel,
'message' => $message,
'node' => $this,
];
}
$fn = $this->select('FN');
if (1 !== count($fn)) {
$repaired = false;
if (($options & self::REPAIR) && 0 === count($fn)) {
// We're going to try to see if we can use the contents of the // We're going to try to see if we can use the contents of the
// N property. // N property.
if (isset($this->N)) { if (isset($this->N)) {
@ -242,26 +284,96 @@ class VCard extends VObject\Document {
} else { } else {
$this->FN = $value[0]; $this->FN = $value[0];
} }
$repaired = true;
// Otherwise, the ORG property may work // Otherwise, the ORG property may work
} elseif (isset($this->ORG)) { } elseif (isset($this->ORG)) {
$this->FN = (string) $this->ORG; $this->FN = (string) $this->ORG;
} $repaired = true;
// Otherwise, the EMAIL property may work
} elseif (isset($this->EMAIL)) {
$this->FN = (string) $this->EMAIL;
$repaired = true;
} }
} }
$warnings[] = [
'level' => $repaired ? 1 : 3,
'message' => 'The FN property must appear in the VCARD component exactly 1 time',
'node' => $this,
];
}
return array_merge( return array_merge(
parent::validate($options), parent::validate($options),
$warnings $warnings
); );
}
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'ADR' => '*',
'ANNIVERSARY' => '?',
'BDAY' => '?',
'CALADRURI' => '*',
'CALURI' => '*',
'CATEGORIES' => '*',
'CLIENTPIDMAP' => '*',
'EMAIL' => '*',
'FBURL' => '*',
'IMPP' => '*',
'GENDER' => '?',
'GEO' => '*',
'KEY' => '*',
'KIND' => '?',
'LANG' => '*',
'LOGO' => '*',
'MEMBER' => '*',
'N' => '?',
'NICKNAME' => '*',
'NOTE' => '*',
'ORG' => '*',
'PHOTO' => '*',
'PRODID' => '?',
'RELATED' => '*',
'REV' => '?',
'ROLE' => '*',
'SOUND' => '*',
'SOURCE' => '*',
'TEL' => '*',
'TITLE' => '*',
'TZ' => '*',
'URL' => '*',
'VERSION' => '1',
'XML' => '*',
// FN is commented out, because it's already handled by the
// validate function, which may also try to repair it.
// 'FN' => '+',
'UID' => '?',
];
} }
/** /**
* Returns a preferred field. * Returns a preferred field.
* *
* VCards can indicate wether a field such as ADR, TEL or EMAIL is * VCards can indicate whether a field such as ADR, TEL or EMAIL is
* preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x
* being a number between 1 and 100). * being a number between 1 and 100).
* *
@ -269,14 +381,14 @@ class VCard extends VObject\Document {
* a field with that name does not exist, null is returned. * a field with that name does not exist, null is returned.
* *
* @param string $fieldName * @param string $fieldName
*
* @return VObject\Property|null * @return VObject\Property|null
*/ */
public function preferred($propertyName) { public function preferred($propertyName)
{
$preferred = null; $preferred = null;
$lastPref = 101; $lastPref = 101;
foreach ($this->select($propertyName) as $field) { foreach ($this->select($propertyName) as $field) {
$pref = 101; $pref = 101;
if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) { if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) {
$pref = 1; $pref = 1;
@ -288,10 +400,29 @@ class VCard extends VObject\Document {
$preferred = $field; $preferred = $field;
$lastPref = $pref; $lastPref = $pref;
} }
} }
return $preferred;
return $preferred;
}
/**
* Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
*
* This function will return null if the property does not exist. If there are
* multiple properties with the same TYPE value, only one will be returned.
*
* @param string $propertyName
* @param string $type
*
* @return VObject\Property|null
*/
public function getByType($propertyName, $type)
{
foreach ($this->select($propertyName) as $field) {
if (isset($field['TYPE']) && $field['TYPE']->has($type)) {
return $field;
}
}
} }
/** /**
@ -299,13 +430,13 @@ class VCard extends VObject\Document {
* *
* @return array * @return array
*/ */
protected function getDefaults() { protected function getDefaults()
{
return array( return [
'VERSION' => '3.0', 'VERSION' => '4.0',
'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
); 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
];
} }
/** /**
@ -314,40 +445,92 @@ class VCard extends VObject\Document {
* *
* @return array * @return array
*/ */
public function jsonSerialize() { #[\ReturnTypeWillChange]
public function jsonSerialize()
{
// A vcard does not have sub-components, so we're overriding this // A vcard does not have sub-components, so we're overriding this
// method to remove that array element. // method to remove that array element.
$properties = array(); $properties = [];
foreach($this->children as $child) { foreach ($this->children() as $child) {
$properties[] = $child->jsonSerialize(); $properties[] = $child->jsonSerialize();
} }
return array( return [
strtolower($this->name), strtolower($this->name),
$properties, $properties,
); ];
}
/**
* This method serializes the data into XML. This is used to create xCard or
* xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
public function xmlSerialize(Xml\Writer $writer)
{
$propertiesByGroup = [];
foreach ($this->children() as $property) {
$group = $property->group;
if (!isset($propertiesByGroup[$group])) {
$propertiesByGroup[$group] = [];
}
$propertiesByGroup[$group][] = $property;
}
$writer->startElement(strtolower($this->name));
foreach ($propertiesByGroup as $group => $properties) {
if (!empty($group)) {
$writer->startElement('group');
$writer->writeAttribute('name', strtolower($group));
}
foreach ($properties as $property) {
switch ($property->name) {
case 'VERSION':
break;
case 'XML':
$value = $property->getParts();
$fragment = new Xml\Element\XmlFragment($value[0]);
$writer->write($fragment);
break;
default:
$property->xmlSerialize($writer);
break;
}
}
if (!empty($group)) {
$writer->endElement();
}
}
$writer->endElement();
} }
/** /**
* Returns the default class for a property name. * Returns the default class for a property name.
* *
* @param string $propertyName * @param string $propertyName
*
* @return string * @return string
*/ */
public function getClassNameForPropertyName($propertyName) { public function getClassNameForPropertyName($propertyName)
{
$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 ($className == 'Sabre\\VObject\\Property\\Binary' && $this->getDocumentType()===self::VCARD40) { return VObject\Property\Uri::class;
return 'Sabre\\VObject\\Property\\Uri';
} }
return $className; return $className;
} }
} }

View file

@ -1,19 +1,23 @@
<?php <?php
namespace Sabre\VObject\Component; namespace Sabre\VObject\Component;
use DateTimeInterface;
use Sabre\VObject; use Sabre\VObject;
use Sabre\VObject\Recur\EventIterator;
use Sabre\VObject\Recur\NoInstancesException;
/** /**
* VEvent component * VEvent component.
* *
* This component contains some additional functionality specific for VEVENT's. * This component contains some additional functionality specific for VEVENT's.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VEvent extends VObject\Component { class VEvent extends VObject\Component
{
/** /**
* Returns true or false depending on if the event falls in the specified * Returns true or false depending on if the event falls in the specified
* time-range. This is used for filtering purposes. * time-range. This is used for filtering purposes.
@ -21,14 +25,19 @@ class VEvent extends VObject\Component {
* The rules used to determine if an event falls within the specified * The rules used to determine if an event falls within the specified
* time-range is based on the CalDAV specification. * time-range is based on the CalDAV specification.
* *
* @param \DateTime $start
* @param \DateTime $end
* @return bool * @return bool
*/ */
public function isInTimeRange(\DateTime $start, \DateTime $end) { public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
{
if ($this->RRULE) { if ($this->RRULE) {
$it = new VObject\RecurrenceIterator($this); try {
$it = new EventIterator($this, null, $start->getTimezone());
} catch (NoInstancesException $e) {
// If we've caught this exception, there are no instances
// for the event that fall into the specified time-range.
return false;
}
$it->fastForward($start); $it->fastForward($start);
// We fast-forwarded to a spot where the end-time of the // We fast-forwarded to a spot where the end-time of the
@ -37,34 +46,95 @@ class VEvent extends VObject\Component {
// //
// If the starttime of the recurrence did not exceed the // If the starttime of the recurrence did not exceed the
// end of the time range as well, we have a match. // end of the time range as well, we have a match.
return ($it->getDTStart() < $end && $it->getDTEnd() > $start); return $it->getDTStart() < $end && $it->getDTEnd() > $start;
} }
$effectiveStart = $this->DTSTART->getDateTime(); $effectiveStart = $this->DTSTART->getDateTime($start->getTimezone());
if (isset($this->DTEND)) { if (isset($this->DTEND)) {
// The DTEND property is considered non inclusive. So for a 3 day // The DTEND property is considered non inclusive. So for a 3 day
// event in july, dtstart and dtend would have to be July 1st and // event in july, dtstart and dtend would have to be July 1st and
// July 4th respectively. // July 4th respectively.
// //
// See: // See:
// http://tools.ietf.org/html/rfc5545#page-54 // http://tools.ietf.org/html/rfc5545#page-54
$effectiveEnd = $this->DTEND->getDateTime(); $effectiveEnd = $this->DTEND->getDateTime($end->getTimezone());
} elseif (isset($this->DURATION)) { } elseif (isset($this->DURATION)) {
$effectiveEnd = clone $effectiveStart; $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
$effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) );
} elseif (!$this->DTSTART->hasTime()) { } elseif (!$this->DTSTART->hasTime()) {
$effectiveEnd = clone $effectiveStart; $effectiveEnd = $effectiveStart->modify('+1 day');
$effectiveEnd->modify('+1 day');
} else { } else {
$effectiveEnd = clone $effectiveStart; $effectiveEnd = $effectiveStart;
}
return (
($start <= $effectiveEnd) && ($end > $effectiveStart)
);
} }
return
($start < $effectiveEnd) && ($end > $effectiveStart)
;
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults()
{
return [
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
];
}
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
$hasMethod = isset($this->parent->METHOD);
return [
'UID' => 1,
'DTSTAMP' => 1,
'DTSTART' => $hasMethod ? '?' : '1',
'CLASS' => '?',
'CREATED' => '?',
'DESCRIPTION' => '?',
'GEO' => '?',
'LAST-MODIFIED' => '?',
'LOCATION' => '?',
'ORGANIZER' => '?',
'PRIORITY' => '?',
'SEQUENCE' => '?',
'STATUS' => '?',
'SUMMARY' => '?',
'TRANSP' => '?',
'URL' => '?',
'RECURRENCE-ID' => '?',
'RRULE' => '?',
'DTEND' => '?',
'DURATION' => '?',
'ATTACH' => '*',
'ATTENDEE' => '*',
'CATEGORIES' => '*',
'COMMENT' => '*',
'CONTACT' => '*',
'EXDATE' => '*',
'REQUEST-STATUS' => '*',
'RELATED-TO' => '*',
'RESOURCES' => '*',
'RDATE' => '*',
];
}
} }

View file

@ -2,35 +2,33 @@
namespace Sabre\VObject\Component; namespace Sabre\VObject\Component;
use DateTimeInterface;
use Sabre\VObject; use Sabre\VObject;
/** /**
* The VFreeBusy component * The VFreeBusy component.
* *
* This component adds functionality to a component, specific for VFREEBUSY * This component adds functionality to a component, specific for VFREEBUSY
* components. * components.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VFreeBusy extends VObject\Component { class VFreeBusy extends VObject\Component
{
/** /**
* Checks based on the contained FREEBUSY information, if a timeslot is * Checks based on the contained FREEBUSY information, if a timeslot is
* available. * available.
* *
* @param DateTime $start
* @param Datetime $end
* @return bool * @return bool
*/ */
public function isFree(\DateTime $start, \Datetime $end) { public function isFree(DateTimeInterface $start, DatetimeInterface $end)
{
foreach ($this->select('FREEBUSY') as $freebusy) { foreach ($this->select('FREEBUSY') as $freebusy) {
// We are only interested in FBTYPE=BUSY (the default), // We are only interested in FBTYPE=BUSY (the default),
// FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') { if (isset($freebusy['FBTYPE']) && 'BUSY' !== strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4))) {
continue; continue;
} }
@ -47,22 +45,49 @@ class VFreeBusy extends VObject\Component {
$busyStart = VObject\DateTimeParser::parse($busyStart); $busyStart = VObject\DateTimeParser::parse($busyStart);
$busyEnd = VObject\DateTimeParser::parse($busyEnd); $busyEnd = VObject\DateTimeParser::parse($busyEnd);
if ($busyEnd instanceof \DateInterval) { if ($busyEnd instanceof \DateInterval) {
$tmp = clone $busyStart; $busyEnd = $busyStart->add($busyEnd);
$tmp->add($busyEnd);
$busyEnd = $tmp;
} }
if ($start < $busyEnd && $end > $busyStart) { if ($start < $busyEnd && $end > $busyStart) {
return false; return false;
} }
} }
} }
return true; return true;
} }
} /**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'UID' => 1,
'DTSTAMP' => 1,
'CONTACT' => '?',
'DTSTART' => '?',
'DTEND' => '?',
'ORGANIZER' => '?',
'URL' => '?',
'ATTENDEE' => '*',
'COMMENT' => '*',
'FREEBUSY' => '*',
'REQUEST-STATUS' => '*',
];
}
}

View file

@ -2,19 +2,20 @@
namespace Sabre\VObject\Component; namespace Sabre\VObject\Component;
use DateTimeInterface;
use Sabre\VObject; use Sabre\VObject;
/** /**
* VJournal component * VJournal component.
* *
* This component contains some additional functionality specific for VJOURNALs. * This component contains some additional functionality specific for VJOURNALs.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VJournal extends VObject\Component { class VJournal extends VObject\Component
{
/** /**
* Returns true or false depending on if the event falls in the specified * Returns true or false depending on if the event falls in the specified
* time-range. This is used for filtering purposes. * time-range. This is used for filtering purposes.
@ -22,25 +23,79 @@ class VJournal extends VObject\Component {
* The rules used to determine if an event falls within the specified * The rules used to determine if an event falls within the specified
* time-range is based on the CalDAV specification. * time-range is based on the CalDAV specification.
* *
* @param DateTime $start
* @param DateTime $end
* @return bool * @return bool
*/ */
public function isInTimeRange(\DateTime $start, \DateTime $end) { public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
{
$dtstart = isset($this->DTSTART) ? $this->DTSTART->getDateTime() : null; $dtstart = isset($this->DTSTART) ? $this->DTSTART->getDateTime() : null;
if ($dtstart) { if ($dtstart) {
$effectiveEnd = clone $dtstart; $effectiveEnd = $dtstart;
if (!$this->DTSTART->hasTime()) { if (!$this->DTSTART->hasTime()) {
$effectiveEnd->modify('+1 day'); $effectiveEnd = $effectiveEnd->modify('+1 day');
} }
return ($start <= $effectiveEnd && $end > $dtstart); return $start <= $effectiveEnd && $end > $dtstart;
} }
return false; return false;
} }
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'UID' => 1,
'DTSTAMP' => 1,
'CLASS' => '?',
'CREATED' => '?',
'DTSTART' => '?',
'LAST-MODIFIED' => '?',
'ORGANIZER' => '?',
'RECURRENCE-ID' => '?',
'SEQUENCE' => '?',
'STATUS' => '?',
'SUMMARY' => '?',
'URL' => '?',
'RRULE' => '?',
'ATTACH' => '*',
'ATTENDEE' => '*',
'CATEGORIES' => '*',
'COMMENT' => '*',
'CONTACT' => '*',
'DESCRIPTION' => '*',
'EXDATE' => '*',
'RELATED-TO' => '*',
'RDATE' => '*',
];
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults()
{
return [
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
];
}
} }

View file

@ -0,0 +1,63 @@
<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
/**
* The VTimeZone component.
*
* This component adds functionality to a component, specific for VTIMEZONE
* components.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VTimeZone extends VObject\Component
{
/**
* Returns the PHP DateTimeZone for this VTIMEZONE component.
*
* If we can't accurately determine the timezone, this method will return
* UTC.
*
* @return \DateTimeZone
*/
public function getTimeZone()
{
return VObject\TimeZoneUtil::getTimeZone((string) $this->TZID, $this->root);
}
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'TZID' => 1,
'LAST-MODIFIED' => '?',
'TZURL' => '?',
// At least 1 STANDARD or DAYLIGHT must appear.
//
// The validator is not specific yet to pick this up, so these
// rules are too loose.
'STANDARD' => '*',
'DAYLIGHT' => '*',
];
}
}

View file

@ -2,19 +2,20 @@
namespace Sabre\VObject\Component; namespace Sabre\VObject\Component;
use DateTimeInterface;
use Sabre\VObject; use Sabre\VObject;
/** /**
* VTodo component * VTodo component.
* *
* This component contains some additional functionality specific for VTODOs. * This component contains some additional functionality specific for VTODOs.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VTodo extends VObject\Component { class VTodo extends VObject\Component
{
/** /**
* Returns true or false depending on if the event falls in the specified * Returns true or false depending on if the event falls in the specified
* time-range. This is used for filtering purposes. * time-range. This is used for filtering purposes.
@ -22,12 +23,10 @@ class VTodo extends VObject\Component {
* The rules used to determine if an event falls within the specified * The rules used to determine if an event falls within the specified
* time-range is based on the CalDAV specification. * time-range is based on the CalDAV specification.
* *
* @param DateTime $start
* @param DateTime $end
* @return bool * @return bool
*/ */
public function isInTimeRange(\DateTime $start, \DateTime $end) { public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
{
$dtstart = isset($this->DTSTART) ? $this->DTSTART->getDateTime() : null; $dtstart = isset($this->DTSTART) ? $this->DTSTART->getDateTime() : null;
$duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null; $duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null;
$due = isset($this->DUE) ? $this->DUE->getDateTime() : null; $due = isset($this->DUE) ? $this->DUE->getDateTime() : null;
@ -36,8 +35,8 @@ class VTodo extends VObject\Component {
if ($dtstart) { if ($dtstart) {
if ($duration) { if ($duration) {
$effectiveEnd = clone $dtstart; $effectiveEnd = $dtstart->add($duration);
$effectiveEnd->add($duration);
return $start <= $effectiveEnd && $end > $dtstart; return $start <= $effectiveEnd && $end > $dtstart;
} elseif ($due) { } elseif ($due) {
return return
@ -48,7 +47,7 @@ class VTodo extends VObject\Component {
} }
} }
if ($due) { if ($due) {
return ($start < $due && $end >= $due); return $start < $due && $end >= $due;
} }
if ($completed && $created) { if ($completed && $created) {
return return
@ -56,13 +55,127 @@ class VTodo extends VObject\Component {
($end >= $created || $end >= $completed); ($end >= $created || $end >= $completed);
} }
if ($completed) { if ($completed) {
return ($start <= $completed && $end >= $completed); return $start <= $completed && $end >= $completed;
} }
if ($created) { if ($created) {
return ($end > $created); return $end > $created;
} }
return true; return true;
} }
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'UID' => 1,
'DTSTAMP' => 1,
'CLASS' => '?',
'COMPLETED' => '?',
'CREATED' => '?',
'DESCRIPTION' => '?',
'DTSTART' => '?',
'GEO' => '?',
'LAST-MODIFIED' => '?',
'LOCATION' => '?',
'ORGANIZER' => '?',
'PERCENT' => '?',
'PRIORITY' => '?',
'RECURRENCE-ID' => '?',
'SEQUENCE' => '?',
'STATUS' => '?',
'SUMMARY' => '?',
'URL' => '?',
'RRULE' => '?',
'DUE' => '?',
'DURATION' => '?',
'ATTACH' => '*',
'ATTENDEE' => '*',
'CATEGORIES' => '*',
'COMMENT' => '*',
'CONTACT' => '*',
'EXDATE' => '*',
'REQUEST-STATUS' => '*',
'RELATED-TO' => '*',
'RESOURCES' => '*',
'RDATE' => '*',
];
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* Node::REPAIR - May attempt to automatically repair the problem.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on)
* 2 - An inconsequential issue
* 3 - A severe issue.
*
* @param int $options
*
* @return array
*/
public function validate($options = 0)
{
$result = parent::validate($options);
if (isset($this->DUE) && isset($this->DTSTART)) {
$due = $this->DUE;
$dtStart = $this->DTSTART;
if ($due->getValueType() !== $dtStart->getValueType()) {
$result[] = [
'level' => 3,
'message' => 'The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART',
'node' => $due,
];
} elseif ($due->getDateTime() < $dtStart->getDateTime()) {
$result[] = [
'level' => 3,
'message' => 'DUE must occur after DTSTART',
'node' => $due,
];
}
}
return $result;
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults()
{
return [
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
'DTSTAMP' => date('Ymd\\THis\\Z'),
];
}
} }

View file

@ -2,67 +2,85 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use DateInterval;
use DateTimeImmutable;
use DateTimeZone;
/** /**
* DateTimeParser * DateTimeParser.
* *
* This class is responsible for parsing the several different date and time * This class is responsible for parsing the several different date and time
* formats iCalendar and vCards have. * formats iCalendar and vCards have.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class DateTimeParser { class DateTimeParser
{
/** /**
* Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object * Parses an iCalendar (rfc5545) formatted datetime and returns a
* DateTimeImmutable object.
* *
* Specifying a reference timezone is optional. It will only be used * Specifying a reference timezone is optional. It will only be used
* if the non-UTC format is used. The argument is used as a reference, the * if the non-UTC format is used. The argument is used as a reference, the
* returned DateTime object will still be in the UTC timezone. * returned DateTimeImmutable object will still be in the UTC timezone.
* *
* @param string $dt * @param string $dt
* @param DateTimeZone $tz * @param DateTimeZone $tz
* @return DateTime *
* @return DateTimeImmutable
*/ */
static public function parseDateTime($dt,\DateTimeZone $tz = null) { public static function parseDateTime($dt, DateTimeZone $tz = null)
{
// Format is YYYYMMDD + "T" + hhmmss // Format is YYYYMMDD + "T" + hhmmss
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches); $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches);
if (!$result) { if (!$result) {
throw new \LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt); throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
} }
if ($matches[7]==='Z' || is_null($tz)) { if ('Z' === $matches[7] || is_null($tz)) {
$tz = new \DateTimeZone('UTC'); $tz = new DateTimeZone('UTC');
}
try {
$date = new DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3].' '.$matches[4].':'.$matches[5].':'.$matches[6], $tz);
} catch (\Exception $e) {
throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
} }
$date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] .':' . $matches[6], $tz);
// Still resetting the timezone, to normalize everything to UTC
// $date->setTimeZone(new \DateTimeZone('UTC'));
return $date; return $date;
} }
/** /**
* Parses an iCalendar (rfc5545) formatted date and returns a DateTime object * Parses an iCalendar (rfc5545) formatted date and returns a DateTimeImmutable object.
* *
* @param string $date * @param string $date
* @return DateTime * @param DateTimeZone $tz
*
* @return DateTimeImmutable
*/ */
static public function parseDate($date) { public static function parseDate($date, DateTimeZone $tz = null)
{
// Format is YYYYMMDD // Format is YYYYMMDD
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches); $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches);
if (!$result) { if (!$result) {
throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date); throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
} }
$date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC')); if (is_null($tz)) {
return $date; $tz = new DateTimeZone('UTC');
}
try {
$date = new DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3], $tz);
} catch (\Exception $e) {
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
}
return $date;
} }
/** /**
@ -73,108 +91,119 @@ class DateTimeParser {
* *
* @param string $duration * @param string $duration
* @param bool $asString * @param bool $asString
*
* @return DateInterval|string * @return DateInterval|string
*/ */
static public function parseDuration($duration, $asString = false) { public static function parseDuration($duration, $asString = false)
{
$result = preg_match('/^(?P<plusminus>\+|-)?P((?P<week>\d+)W)?((?P<day>\d+)D)?(T((?P<hour>\d+)H)?((?P<minute>\d+)M)?((?P<second>\d+)S)?)?$/', $duration, $matches); $result = preg_match('/^(?<plusminus>\+|-)?P((?<week>\d+)W)?((?<day>\d+)D)?(T((?<hour>\d+)H)?((?<minute>\d+)M)?((?<second>\d+)S)?)?$/', $duration, $matches);
if (!$result) { if (!$result) {
throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration); throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration);
} }
if (!$asString) { if (!$asString) {
$invert = false; $invert = false;
if ($matches['plusminus']==='-') {
if ('-' === $matches['plusminus']) {
$invert = true; $invert = true;
} }
$parts = [
$parts = array(
'week', 'week',
'day', 'day',
'hour', 'hour',
'minute', 'minute',
'second', 'second',
); ];
foreach ($parts as $part) { foreach ($parts as $part) {
$matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0;
} }
// We need to re-construct the $duration string, because weeks and // We need to re-construct the $duration string, because weeks and
// days are not supported by DateInterval in the same string. // days are not supported by DateInterval in the same string.
$duration = 'P'; $duration = 'P';
$days = $matches['day']; $days = $matches['day'];
if ($matches['week']) { if ($matches['week']) {
$days += $matches['week'] * 7; $days += $matches['week'] * 7;
} }
if ($days)
if ($days) {
$duration .= $days.'D'; $duration .= $days.'D';
}
if ($matches['minute'] || $matches['second'] || $matches['hour']) { if ($matches['minute'] || $matches['second'] || $matches['hour']) {
$duration .= 'T'; $duration .= 'T';
if ($matches['hour']) if ($matches['hour']) {
$duration .= $matches['hour'].'H'; $duration .= $matches['hour'].'H';
if ($matches['minute'])
$duration.=$matches['minute'].'M';
if ($matches['second'])
$duration.=$matches['second'].'S';
} }
if ($duration==='P') { if ($matches['minute']) {
$duration .= $matches['minute'].'M';
}
if ($matches['second']) {
$duration .= $matches['second'].'S';
}
}
if ('P' === $duration) {
$duration = 'PT0S'; $duration = 'PT0S';
} }
$iv = new \DateInterval($duration);
if ($invert) $iv->invert = true;
return $iv; $iv = new DateInterval($duration);
if ($invert) {
$iv->invert = true;
} }
return $iv;
}
$parts = [
$parts = array(
'week', 'week',
'day', 'day',
'hour', 'hour',
'minute', 'minute',
'second', 'second',
); ];
$newDur = ''; $newDur = '';
foreach ($parts as $part) { foreach ($parts as $part) {
if (isset($matches[$part]) && $matches[$part]) { if (isset($matches[$part]) && $matches[$part]) {
$newDur .= ' '.$matches[$part].' '.$part.'s'; $newDur .= ' '.$matches[$part].' '.$part.'s';
} }
} }
$newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur); $newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur);
if ($newDur === '+') { $newDur = '+0 seconds'; };
return $newDur;
if ('+' === $newDur) {
$newDur = '+0 seconds';
}
return $newDur;
} }
/** /**
* Parses either a Date or DateTime, or Duration value. * Parses either a Date or DateTime, or Duration value.
* *
* @param string $date * @param string $date
* @param DateTimeZone|string $referenceTZ * @param DateTimeZone|string $referenceTz
* @return DateTime|DateInterval *
* @return DateTimeImmutable|DateInterval
*/ */
static public function parse($date, $referenceTZ = null) { public static function parse($date, $referenceTz = null)
{
if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) { if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) {
return self::parseDuration($date); return self::parseDuration($date);
} elseif (strlen($date)===8) { } elseif (8 === strlen($date)) {
return self::parseDate($date); return self::parseDate($date, $referenceTz);
} else { } else {
return self::parseDateTime($date, $referenceTZ); return self::parseDateTime($date, $referenceTz);
} }
} }
/** /**
@ -191,7 +220,7 @@ class DateTimeParser {
* Almost any part of the string may be omitted. It's for example legal to * Almost any part of the string may be omitted. It's for example legal to
* just specify seconds, leave out the year, etc. * just specify seconds, leave out the year, etc.
* *
* Timezone is either returned as 'Z' or as '+08:00' * Timezone is either returned as 'Z' or as '+0800'
* *
* For any non-specified values null is returned. * For any non-specified values null is returned.
* *
@ -230,23 +259,25 @@ class DateTimeParser {
* UTC, or a string like -0500 or +1100. * UTC, or a string like -0500 or +1100.
* *
* @param string $date * @param string $date
*
* @return array * @return array
*/ */
static public function parseVCardDateTime($date) { public static function parseVCardDateTime($date)
{
$regex = '/^ $regex = '/^
(?: # date part (?: # date part
(?: (?:
(?: (?P<year> [0-9]{4}) (?: -)?| --) (?: (?<year> [0-9]{4}) (?: -)?| --)
(?P<month> [0-9]{2})? (?<month> [0-9]{2})?
|---) |---)
(?P<date> [0-9]{2})? (?<date> [0-9]{2})?
)? )?
(?:T # time part (?:T # time part
(?P<hour> [0-9]{2} | -) (?<hour> [0-9]{2} | -)
(?P<minute> [0-9]{2} | -)? (?<minute> [0-9]{2} | -)?
(?P<second> [0-9]{2})? (?<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset (?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{4}) Z | (?: \+|-)(?: [0-9]{4})
@ -256,22 +287,21 @@ class DateTimeParser {
)? )?
$/x'; $/x';
if (!preg_match($regex, $date, $matches)) { if (!preg_match($regex, $date, $matches)) {
// Attempting to parse the extended format. // Attempting to parse the extended format.
$regex = '/^ $regex = '/^
(?: # date part (?: # date part
(?: (?P<year> [0-9]{4}) - | -- ) (?: (?<year> [0-9]{4}) - | -- )
(?P<month> [0-9]{2}) - (?<month> [0-9]{2}) -
(?P<date> [0-9]{2}) (?<date> [0-9]{2})
)? )?
(?:T # time part (?:T # time part
(?: (?P<hour> [0-9]{2}) : | -) (?: (?<hour> [0-9]{2}) : | -)
(?: (?P<minute> [0-9]{2}) : | -)? (?: (?<minute> [0-9]{2}) : | -)?
(?P<second> [0-9]{2})? (?<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset (?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
@ -282,35 +312,31 @@ class DateTimeParser {
$/x'; $/x';
if (!preg_match($regex, $date, $matches)) { if (!preg_match($regex, $date, $matches)) {
throw new \InvalidArgumentException('Invalid vCard date-time string: ' . $date); throw new InvalidDataException('Invalid vCard date-time string: '.$date);
} }
} }
$parts = array( $parts = [
'year', 'year',
'month', 'month',
'date', 'date',
'hour', 'hour',
'minute', 'minute',
'second', 'second',
'timezone' 'timezone',
); ];
$result = array(); $result = [];
foreach ($parts as $part) { foreach ($parts as $part) {
if (empty($matches[$part])) { if (empty($matches[$part])) {
$result[$part] = null; $result[$part] = null;
} elseif ($matches[$part] === '-' || $matches[$part] === '--') { } elseif ('-' === $matches[$part] || '--' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
} else { } else {
$result[$part] = $matches[$part]; $result[$part] = $matches[$part];
} }
} }
return $result; return $result;
} }
/** /**
@ -352,15 +378,17 @@ class DateTimeParser {
* UTC, or a string like -0500 or +11:00. * UTC, or a string like -0500 or +11:00.
* *
* @param string $date * @param string $date
*
* @return array * @return array
*/ */
static public function parseVCardTime($date) { public static function parseVCardTime($date)
{
$regex = '/^ $regex = '/^
(?P<hour> [0-9]{2} | -) (?<hour> [0-9]{2} | -)
(?P<minute> [0-9]{2} | -)? (?<minute> [0-9]{2} | -)?
(?P<second> [0-9]{2})? (?<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset (?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{4}) Z | (?: \+|-)(?: [0-9]{4})
@ -368,15 +396,14 @@ class DateTimeParser {
)? )?
$/x'; $/x';
if (!preg_match($regex, $date, $matches)) { if (!preg_match($regex, $date, $matches)) {
// Attempting to parse the extended format. // Attempting to parse the extended format.
$regex = '/^ $regex = '/^
(?: (?P<hour> [0-9]{2}) : | -) (?: (?<hour> [0-9]{2}) : | -)
(?: (?P<minute> [0-9]{2}) : | -)? (?: (?<minute> [0-9]{2}) : | -)?
(?P<second> [0-9]{2})? (?<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset (?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
@ -385,31 +412,149 @@ class DateTimeParser {
$/x'; $/x';
if (!preg_match($regex, $date, $matches)) { if (!preg_match($regex, $date, $matches)) {
throw new \InvalidArgumentException('Invalid vCard time string: ' . $date); throw new InvalidDataException('Invalid vCard time string: '.$date);
} }
} }
$parts = array( $parts = [
'hour', 'hour',
'minute', 'minute',
'second', 'second',
'timezone' 'timezone',
); ];
$result = array(); $result = [];
foreach ($parts as $part) { foreach ($parts as $part) {
if (empty($matches[$part])) { if (empty($matches[$part])) {
$result[$part] = null; $result[$part] = null;
} elseif ($matches[$part] === '-') { } elseif ('-' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
} else { } else {
$result[$part] = $matches[$part]; $result[$part] = $matches[$part];
} }
} }
return $result; return $result;
}
/**
* This method parses a vCard date and or time value.
*
* This can be used for the DATE, DATE-TIME and
* DATE-AND-OR-TIME value.
*
* This method returns an array, not a DateTime value.
* The elements in the array are in the following order:
* year, month, date, hour, minute, second, timezone
* Almost any part of the string may be omitted. It's for example legal to
* just specify seconds, leave out the year, etc.
*
* Timezone is either returned as 'Z' or as '+0800'
*
* For any non-specified values null is returned.
*
* List of date formats that are supported:
* 20150128
* 2015-01
* --01
* --0128
* ---28
*
* List of supported time formats:
* 13
* 1353
* 135301
* -53
* -5301
* --01 (unreachable, see the tests)
* --01Z
* --01+1234
*
* List of supported date-time formats:
* 20150128T13
* --0128T13
* ---28T13
* ---28T1353
* ---28T135301
* ---28T13Z
* ---28T13+1234
*
* See the regular expressions for all the possible patterns.
*
* Times may be postfixed by a timezone offset. This can be either 'Z' for
* UTC, or a string like -0500 or +1100.
*
* @param string $date
*
* @return array
*/
public static function parseVCardDateAndOrTime($date)
{
// \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d
$valueDate = '/^(?J)(?:'.
'(?<year>\d{4})(?<month>\d\d)(?<date>\d\d)'.
'|(?<year>\d{4})-(?<month>\d\d)'.
'|--(?<month>\d\d)(?<date>\d\d)?'.
'|---(?<date>\d\d)'.
')$/';
// (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)?
$valueTime = '/^(?J)(?:'.
'((?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?'.
'|-(?<minute>\d\d)(?<second>\d\d)?'.
'|--(?<second>\d\d))'.
'(?<timezone>(Z|[+\-]\d\d(\d\d)?))?'.
')$/';
// (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)?
$valueDateTime = '/^(?:'.
'((?<year0>\d{4})(?<month0>\d\d)(?<date0>\d\d)'.
'|--(?<month1>\d\d)(?<date1>\d\d)'.
'|---(?<date2>\d\d))'.
'T'.
'(?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?'.
'(?<timezone>(Z|[+\-]\d\d(\d\d?)))?'.
')$/';
// date-and-or-time is date | date-time | time
// in this strict order.
if (0 === preg_match($valueDate, $date, $matches)
&& 0 === preg_match($valueDateTime, $date, $matches)
&& 0 === preg_match($valueTime, $date, $matches)) {
throw new InvalidDataException('Invalid vCard date-time string: '.$date);
}
$parts = [
'year' => null,
'month' => null,
'date' => null,
'hour' => null,
'minute' => null,
'second' => null,
'timezone' => null,
];
// The $valueDateTime expression has a bug with (?J) so we simulate it.
$parts['date0'] = &$parts['date'];
$parts['date1'] = &$parts['date'];
$parts['date2'] = &$parts['date'];
$parts['month0'] = &$parts['month'];
$parts['month1'] = &$parts['month'];
$parts['year0'] = &$parts['year'];
foreach ($parts as $part => &$value) {
if (!empty($matches[$part])) {
$value = $matches[$part];
}
}
unset($parts['date0']);
unset($parts['date1']);
unset($parts['date2']);
unset($parts['month0']);
unset($parts['month1']);
unset($parts['year0']);
return $parts;
} }
} }

View file

@ -3,7 +3,7 @@
namespace Sabre\VObject; namespace Sabre\VObject;
/** /**
* Document * Document.
* *
* A document is just like a component, except that it's also the top level * A document is just like a component, except that it's also the top level
* element. * element.
@ -12,39 +12,39 @@ namespace Sabre\VObject;
* *
* This class also provides a registry for document types. * This class also provides a registry for document types.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
abstract class Document extends Component { abstract class Document extends Component
{
/** /**
* Unknown document type * Unknown document type.
*/ */
const UNKNOWN = 1; const UNKNOWN = 1;
/** /**
* vCalendar 1.0 * vCalendar 1.0.
*/ */
const VCALENDAR10 = 2; const VCALENDAR10 = 2;
/** /**
* iCalendar 2.0 * iCalendar 2.0.
*/ */
const ICALENDAR20 = 3; const ICALENDAR20 = 3;
/** /**
* vCard 2.1 * vCard 2.1.
*/ */
const VCARD21 = 4; const VCARD21 = 4;
/** /**
* vCard 3.0 * vCard 3.0.
*/ */
const VCARD30 = 5; const VCARD30 = 5;
/** /**
* vCard 4.0 * vCard 4.0.
*/ */
const VCARD40 = 6; const VCARD40 = 6;
@ -55,28 +55,28 @@ abstract class Document extends Component {
* *
* @var string * @var string
*/ */
static public $defaultName; public static $defaultName;
/** /**
* List of properties, and which classes they map to. * List of properties, and which classes they map to.
* *
* @var array * @var array
*/ */
static public $propertyMap = array(); public static $propertyMap = [];
/** /**
* List of components, along with which classes they map to. * List of components, along with which classes they map to.
* *
* @var array * @var array
*/ */
static public $componentMap = array(); public static $componentMap = [];
/** /**
* List of value-types, and which classes they map to. * List of value-types, and which classes they map to.
* *
* @var array * @var array
*/ */
static public $valueMap = array(); public static $valueMap = [];
/** /**
* Creates a new document. * Creates a new document.
@ -89,33 +89,32 @@ abstract class Document extends Component {
* *
* So the two sigs: * So the two sigs:
* *
* new Document(array $children = array(), $defaults = true); * new Document(array $children = [], $defaults = true);
* new Document(string $name, array $children = array(), $defaults = true) * new Document(string $name, array $children = [], $defaults = true)
*
* @return void
*/ */
public function __construct() { public function __construct()
{
$args = func_get_args(); $args = func_get_args();
if (count($args)===0 || is_array($args[0])) { $name = static::$defaultName;
array_unshift($args, $this, static::$defaultName); if (0 === count($args) || is_array($args[0])) {
call_user_func_array(array('parent', '__construct'), $args); $children = isset($args[0]) ? $args[0] : [];
$defaults = isset($args[1]) ? $args[1] : true;
} else { } else {
array_unshift($args, $this); $name = $args[0];
call_user_func_array(array('parent', '__construct'), $args); $children = isset($args[1]) ? $args[1] : [];
$defaults = isset($args[2]) ? $args[2] : true;
} }
parent::__construct($this, $name, $children, $defaults);
} }
/** /**
* Returns the current document type. * Returns the current document type.
* *
* @return void * @return int
*/ */
public function getDocumentType() { public function getDocumentType()
{
return self::UNKNOWN; return self::UNKNOWN;
} }
/** /**
@ -126,24 +125,20 @@ abstract class Document extends Component {
* *
* @param string $name * @param string $name
* @param string $arg1,... Unlimited number of args * @param string $arg1,... Unlimited number of args
*
* @return mixed * @return mixed
*/ */
public function create($name) { public function create($name)
{
if (isset(static::$componentMap[strtoupper($name)])) { if (isset(static::$componentMap[strtoupper($name)])) {
return call_user_func_array([$this, 'createComponent'], func_get_args());
return call_user_func_array(array($this,'createComponent'), func_get_args());
} else { } else {
return call_user_func_array([$this, 'createProperty'], func_get_args());
return call_user_func_array(array($this,'createProperty'), func_get_args());
} }
} }
/** /**
* Creates a new component * Creates a new component.
* *
* This method automatically searches for the correct component class, based * This method automatically searches for the correct component class, based
* on its name. * on its name.
@ -159,23 +154,26 @@ abstract class Document extends Component {
* @param string $name * @param string $name
* @param array $children * @param array $children
* @param bool $defaults * @param bool $defaults
*
* @return Component * @return Component
*/ */
public function createComponent($name, array $children = null, $defaults = true) { public function createComponent($name, array $children = null, $defaults = true)
{
$name = strtoupper($name); $name = strtoupper($name);
$class = 'Sabre\\VObject\\Component'; $class = Component::class;
if (isset(static::$componentMap[$name])) { if (isset(static::$componentMap[$name])) {
$class = static::$componentMap[$name]; $class = static::$componentMap[$name];
} }
if (is_null($children)) $children = array(); if (is_null($children)) {
return new $class($this, $name, $children, $defaults); $children = [];
}
return new $class($this, $name, $children, $defaults);
} }
/** /**
* Factory method for creating new properties * Factory method for creating new properties.
* *
* This method automatically searches for the correct property class, based * This method automatically searches for the correct property class, based
* on its name. * on its name.
@ -188,12 +186,13 @@ abstract class Document extends Component {
* @param mixed $value * @param mixed $value
* @param array $parameters * @param array $parameters
* @param string $valueType Force a specific valuetype, such as URI or TEXT * @param string $valueType Force a specific valuetype, such as URI or TEXT
*
* @return Property * @return Property
*/ */
public function createProperty($name, $value = null, array $parameters = null, $valueType = null) { public function createProperty($name, $value = null, array $parameters = null, $valueType = null)
{
// If there's a . in the name, it means it's prefixed by a groupname. // If there's a . in the name, it means it's prefixed by a groupname.
if (($i=strpos($name,'.'))!==false) { if (false !== ($i = strpos($name, '.'))) {
$group = substr($name, 0, $i); $group = substr($name, 0, $i);
$name = strtoupper(substr($name, $i + 1)); $name = strtoupper(substr($name, $i + 1));
} else { } else {
@ -209,17 +208,22 @@ abstract class Document extends Component {
$class = $this->getClassNameForPropertyValue($valueType); $class = $this->getClassNameForPropertyValue($valueType);
} }
if (is_null($class) && isset($parameters['VALUE'])) {
// If a VALUE parameter is supplied, we should use that.
$class = $this->getClassNameForPropertyValue($parameters['VALUE']);
}
if (is_null($class)) { if (is_null($class)) {
// If a VALUE parameter is supplied, we should use that.
if (isset($parameters['VALUE'])) {
$class = $this->getClassNameForPropertyValue($parameters['VALUE']);
if (is_null($class)) {
throw new InvalidDataException('Unsupported VALUE parameter for '.$name.' property. You supplied "'.$parameters['VALUE'].'"');
}
} else {
$class = $this->getClassNameForPropertyName($name); $class = $this->getClassNameForPropertyName($name);
} }
if (is_null($parameters)) $parameters = array(); }
if (is_null($parameters)) {
$parameters = [];
}
return new $class($this, $name, $value, $parameters, $group); return new $class($this, $name, $value, $parameters, $group);
} }
/** /**
@ -231,31 +235,30 @@ abstract class Document extends Component {
* This method returns null if we don't have a specialized class. * This method returns null if we don't have a specialized class.
* *
* @param string $valueParam * @param string $valueParam
* @return void *
* @return string|null
*/ */
public function getClassNameForPropertyValue($valueParam) { public function getClassNameForPropertyValue($valueParam)
{
$valueParam = strtoupper($valueParam); $valueParam = strtoupper($valueParam);
if (isset(static::$valueMap[$valueParam])) { if (isset(static::$valueMap[$valueParam])) {
return static::$valueMap[$valueParam]; return static::$valueMap[$valueParam];
} }
} }
/** /**
* Returns the default class for a property name. * Returns the default class for a property name.
* *
* @param string $propertyName * @param string $propertyName
*
* @return string * @return string
*/ */
public function getClassNameForPropertyName($propertyName) { public function getClassNameForPropertyName($propertyName)
{
if (isset(static::$propertyMap[$propertyName])) { if (isset(static::$propertyMap[$propertyName])) {
return static::$propertyMap[$propertyName]; return static::$propertyMap[$propertyName];
} else { } else {
return 'Sabre\\VObject\\Property\\Unknown'; return Property\Unknown::class;
} }
} }
} }

View file

@ -2,155 +2,33 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use ArrayIterator;
use LogicException;
/** /**
* VObject ElementList * VObject ElementList.
* *
* This class represents a list of elements. Lists are the result of queries, * This class represents a list of elements. Lists are the result of queries,
* such as doing $vcalendar->vevent where there's multiple VEVENT objects. * such as doing $vcalendar->vevent where there's multiple VEVENT objects.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class ElementList implements \Iterator, \Countable, \ArrayAccess { class ElementList extends ArrayIterator
{
/**
* Inner elements
*
* @var array
*/
protected $elements = array();
/**
* Creates the element list.
*
* @param array $elements
*/
public function __construct(array $elements) {
$this->elements = $elements;
}
/* {{{ Iterator interface */
/**
* Current position
*
* @var int
*/
private $key = 0;
/**
* Returns current item in iteration
*
* @return Element
*/
public function current() {
return $this->elements[$this->key];
}
/**
* To the next item in the iterator
*
* @return void
*/
public function next() {
$this->key++;
}
/**
* Returns the current iterator key
*
* @return int
*/
public function key() {
return $this->key;
}
/**
* Returns true if the current position in the iterator is a valid one
*
* @return bool
*/
public function valid() {
return isset($this->elements[$this->key]);
}
/**
* Rewinds the iterator
*
* @return void
*/
public function rewind() {
$this->key = 0;
}
/* }}} */
/* {{{ Countable interface */
/**
* Returns the number of elements
*
* @return int
*/
public function count() {
return count($this->elements);
}
/* }}} */
/* {{{ ArrayAccess Interface */ /* {{{ ArrayAccess Interface */
/**
* Checks if an item exists through ArrayAccess.
*
* @param int $offset
* @return bool
*/
public function offsetExists($offset) {
return isset($this->elements[$offset]);
}
/**
* Gets an item through ArrayAccess.
*
* @param int $offset
* @return mixed
*/
public function offsetGet($offset) {
return $this->elements[$offset];
}
/** /**
* Sets an item through ArrayAccess. * Sets an item through ArrayAccess.
* *
* @param int $offset * @param int $offset
* @param mixed $value * @param mixed $value
* @return void
*/ */
public function offsetSet($offset,$value) { #[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
throw new \LogicException('You can not add new objects to an ElementList'); {
throw new LogicException('You can not add new objects to an ElementList');
} }
/** /**
@ -159,14 +37,12 @@ class ElementList implements \Iterator, \Countable, \ArrayAccess {
* This method just forwards the request to the inner iterator * This method just forwards the request to the inner iterator
* *
* @param int $offset * @param int $offset
* @return void
*/ */
public function offsetUnset($offset) { #[\ReturnTypeWillChange]
public function offsetUnset($offset)
throw new \LogicException('You can not remove objects from an ElementList'); {
throw new LogicException('You can not remove objects from an ElementList');
} }
/* }}} */ /* }}} */
} }

View file

@ -6,8 +6,10 @@ namespace Sabre\VObject;
* Exception thrown by parser when the end of the stream has been reached, * Exception thrown by parser when the end of the stream has been reached,
* before this was expected. * before this was expected.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class EofException extends ParseException { } class EofException extends ParseException
{
}

View file

@ -0,0 +1,185 @@
<?php
namespace Sabre\VObject;
/**
* FreeBusyData is a helper class that manages freebusy information.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class FreeBusyData
{
/**
* Start timestamp.
*
* @var int
*/
protected $start;
/**
* End timestamp.
*
* @var int
*/
protected $end;
/**
* A list of free-busy times.
*
* @var array
*/
protected $data;
public function __construct($start, $end)
{
$this->start = $start;
$this->end = $end;
$this->data = [];
$this->data[] = [
'start' => $this->start,
'end' => $this->end,
'type' => 'FREE',
];
}
/**
* Adds free or busytime to the data.
*
* @param int $start
* @param int $end
* @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE
*/
public function add($start, $end, $type)
{
if ($start > $this->end || $end < $this->start) {
// This new data is outside our timerange.
return;
}
if ($start < $this->start) {
// The item starts before our requested time range
$start = $this->start;
}
if ($end > $this->end) {
// The item ends after our requested time range
$end = $this->end;
}
// Finding out where we need to insert the new item.
$currentIndex = 0;
while ($start > $this->data[$currentIndex]['end']) {
++$currentIndex;
}
// The standard insertion point will be one _after_ the first
// overlapping item.
$insertStartIndex = $currentIndex + 1;
$newItem = [
'start' => $start,
'end' => $end,
'type' => $type,
];
$precedingItem = $this->data[$insertStartIndex - 1];
if ($this->data[$insertStartIndex - 1]['start'] === $start) {
// The old item starts at the exact same point as the new item.
--$insertStartIndex;
}
// Now we know where to insert the item, we need to know where it
// starts overlapping with items on the tail end. We need to start
// looking one item before the insertStartIndex, because it's possible
// that the new item 'sits inside' the previous old item.
if ($insertStartIndex > 0) {
$currentIndex = $insertStartIndex - 1;
} else {
$currentIndex = 0;
}
while ($end > $this->data[$currentIndex]['end']) {
++$currentIndex;
}
// What we are about to insert into the array
$newItems = [
$newItem,
];
// This is the amount of items that are completely overwritten by the
// new item.
$itemsToDelete = $currentIndex - $insertStartIndex;
if ($this->data[$currentIndex]['end'] <= $end) {
++$itemsToDelete;
}
// If itemsToDelete was -1, it means that the newly inserted item is
// actually sitting inside an existing one. This means we need to split
// the item at the current position in two and insert the new item in
// between.
if (-1 === $itemsToDelete) {
$itemsToDelete = 0;
if ($newItem['end'] < $precedingItem['end']) {
$newItems[] = [
'start' => $newItem['end'] + 1,
'end' => $precedingItem['end'],
'type' => $precedingItem['type'],
];
}
}
array_splice(
$this->data,
$insertStartIndex,
$itemsToDelete,
$newItems
);
$doMerge = false;
$mergeOffset = $insertStartIndex;
$mergeItem = $newItem;
$mergeDelete = 1;
if (isset($this->data[$insertStartIndex - 1])) {
// Updating the start time of the previous item.
$this->data[$insertStartIndex - 1]['end'] = $start;
// If the previous and the current are of the same type, we can
// merge them into one item.
if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) {
$doMerge = true;
--$mergeOffset;
++$mergeDelete;
$mergeItem['start'] = $this->data[$insertStartIndex - 1]['start'];
}
}
if (isset($this->data[$insertStartIndex + 1])) {
// Updating the start time of the next item.
$this->data[$insertStartIndex + 1]['start'] = $end;
// If the next and the current are of the same type, we can
// merge them into one item.
if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) {
$doMerge = true;
++$mergeDelete;
$mergeItem['end'] = $this->data[$insertStartIndex + 1]['end'];
}
}
if ($doMerge) {
array_splice(
$this->data,
$mergeOffset,
$mergeDelete,
[$mergeItem]
);
}
}
public function getData()
{
return $this->data;
}
}

View file

@ -2,8 +2,12 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use use DateTimeImmutable;
Sabre\VObject\Component\VCalendar; use DateTimeInterface;
use DateTimeZone;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Recur\EventIterator;
use Sabre\VObject\Recur\NoInstancesException;
/** /**
* This class helps with generating FREEBUSY reports based on existing sets of * This class helps with generating FREEBUSY reports based on existing sets of
@ -15,61 +19,87 @@ use
* VFREEBUSY components are described in RFC5545, The rules for what should * VFREEBUSY components are described in RFC5545, The rules for what should
* go in a single freebusy report is taken from RFC4791, section 7.10. * go in a single freebusy report is taken from RFC4791, section 7.10.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class FreeBusyGenerator { class FreeBusyGenerator
{
/** /**
* Input objects * Input objects.
* *
* @var array * @var array
*/ */
protected $objects; protected $objects = [];
/** /**
* Start of range * Start of range.
* *
* @var DateTime|null * @var DateTimeInterface|null
*/ */
protected $start; protected $start;
/** /**
* End of range * End of range.
* *
* @var DateTime|null * @var DateTimeInterface|null
*/ */
protected $end; protected $end;
/** /**
* VCALENDAR object * VCALENDAR object.
* *
* @var Component * @var Document
*/ */
protected $baseObject; protected $baseObject;
/**
* Reference timezone.
*
* When we are calculating busy times, and we come across so-called
* floating times (times without a timezone), we use the reference timezone
* instead.
*
* This is also used for all-day events.
*
* This defaults to UTC.
*
* @var DateTimeZone
*/
protected $timeZone;
/**
* A VAVAILABILITY document.
*
* If this is set, its information will be included when calculating
* freebusy time.
*
* @var Document
*/
protected $vavailability;
/** /**
* Creates the generator. * Creates the generator.
* *
* Check the setTimeRange and setObjects methods for details about the * Check the setTimeRange and setObjects methods for details about the
* arguments. * arguments.
* *
* @param DateTime $start * @param DateTimeInterface $start
* @param DateTime $end * @param DateTimeInterface $end
* @param mixed $objects * @param mixed $objects
* @return void * @param DateTimeZone $timeZone
*/ */
public function __construct(\DateTime $start = null, \DateTime $end = null, $objects = null) { public function __construct(DateTimeInterface $start = null, DateTimeInterface $end = null, $objects = null, DateTimeZone $timeZone = null)
{
if ($start && $end) {
$this->setTimeRange($start, $end); $this->setTimeRange($start, $end);
}
if ($objects) { if ($objects) {
$this->setObjects($objects); $this->setObjects($objects);
} }
if (is_null($timeZone)) {
$timeZone = new DateTimeZone('UTC');
}
$this->setTimeZone($timeZone);
} }
/** /**
@ -79,61 +109,73 @@ class FreeBusyGenerator {
* for setting things like the METHOD, CALSCALE, VERSION, etc.. * for setting things like the METHOD, CALSCALE, VERSION, etc..
* *
* The VFREEBUSY object will be automatically added though. * The VFREEBUSY object will be automatically added though.
*
* @param Component $vcalendar
* @return void
*/ */
public function setBaseObject(Component $vcalendar) { public function setBaseObject(Document $vcalendar)
{
$this->baseObject = $vcalendar; $this->baseObject = $vcalendar;
} }
/** /**
* Sets the input objects * Sets a VAVAILABILITY document.
*/
public function setVAvailability(Document $vcalendar)
{
$this->vavailability = $vcalendar;
}
/**
* Sets the input objects.
* *
* You must either specify a valendar object as a strong, or as the parse * You must either specify a vcalendar object as a string, or as the parse
* Component. * Component.
* It's also possible to specify multiple objects as an array. * It's also possible to specify multiple objects as an array.
* *
* @param mixed $objects * @param mixed $objects
* @return void
*/ */
public function setObjects($objects) { public function setObjects($objects)
{
if (!is_array($objects)) { if (!is_array($objects)) {
$objects = array($objects); $objects = [$objects];
} }
$this->objects = array(); $this->objects = [];
foreach ($objects as $object) { foreach ($objects as $object) {
if (is_string($object) || is_resource($object)) {
if (is_string($object)) {
$this->objects[] = Reader::read($object); $this->objects[] = Reader::read($object);
} elseif ($object instanceof Component) { } elseif ($object instanceof Component) {
$this->objects[] = $object; $this->objects[] = $object;
} else { } else {
throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects');
} }
} }
} }
/** /**
* Sets the time range * Sets the time range.
* *
* Any freebusy object falling outside of this time range will be ignored. * Any freebusy object falling outside of this time range will be ignored.
* *
* @param DateTime $start * @param DateTimeInterface $start
* @param DateTime $end * @param DateTimeInterface $end
* @return void
*/ */
public function setTimeRange(\DateTime $start = null, \DateTime $end = null) { public function setTimeRange(DateTimeInterface $start = null, DateTimeInterface $end = null)
{
if (!$start) {
$start = new DateTimeImmutable(Settings::$minDate);
}
if (!$end) {
$end = new DateTimeImmutable(Settings::$maxDate);
}
$this->start = $start; $this->start = $start;
$this->end = $end; $this->end = $end;
}
/**
* Sets the reference timezone for floating times.
*/
public function setTimeZone(DateTimeZone $timeZone)
{
$this->timeZone = $timeZone;
} }
/** /**
@ -142,141 +184,317 @@ class FreeBusyGenerator {
* *
* @return Component * @return Component
*/ */
public function getResult() { public function getResult()
{
$fbData = new FreeBusyData(
$this->start->getTimeStamp(),
$this->end->getTimeStamp()
);
if ($this->vavailability) {
$this->calculateAvailability($fbData, $this->vavailability);
}
$busyTimes = array(); $this->calculateBusy($fbData, $this->objects);
foreach($this->objects as $object) { return $this->generateFreeBusyCalendar($fbData);
}
/**
* This method takes a VAVAILABILITY component and figures out all the
* available times.
*/
protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability)
{
$vavailComps = iterator_to_array($vavailability->VAVAILABILITY);
usort(
$vavailComps,
function ($a, $b) {
// We need to order the components by priority. Priority 1
// comes first, up until priority 9. Priority 0 comes after
// priority 9. No priority implies priority 0.
//
// Yes, I'm serious.
$priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0;
$priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0;
if (0 === $priorityA) {
$priorityA = 10;
}
if (0 === $priorityB) {
$priorityB = 10;
}
return $priorityA - $priorityB;
}
);
// Now we go over all the VAVAILABILITY components and figure if
// there's any we don't need to consider.
//
// This is can be because of one of two reasons: either the
// VAVAILABILITY component falls outside the time we are interested in,
// or a different VAVAILABILITY component with a higher priority has
// already completely covered the time-range.
$old = $vavailComps;
$new = [];
foreach ($old as $vavail) {
list($compStart, $compEnd) = $vavail->getEffectiveStartEnd();
// We don't care about datetimes that are earlier or later than the
// start and end of the freebusy report, so this gets normalized
// first.
if (is_null($compStart) || $compStart < $this->start) {
$compStart = $this->start;
}
if (is_null($compEnd) || $compEnd > $this->end) {
$compEnd = $this->end;
}
// If the item fell out of the timerange, we can just skip it.
if ($compStart > $this->end || $compEnd < $this->start) {
continue;
}
// Going through our existing list of components to see if there's
// a higher priority component that already fully covers this one.
foreach ($new as $higherVavail) {
list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd();
if (
(is_null($higherStart) || $higherStart < $compStart) &&
(is_null($higherEnd) || $higherEnd > $compEnd)
) {
// Component is fully covered by a higher priority
// component. We can skip this component.
continue 2;
}
}
// We're keeping it!
$new[] = $vavail;
}
// Lastly, we need to traverse the remaining components and fill in the
// freebusydata slots.
//
// We traverse the components in reverse, because we want the higher
// priority components to override the lower ones.
foreach (array_reverse($new) as $vavail) {
$busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE';
list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd();
// Making the component size no larger than the requested free-busy
// report range.
if (!$vavailStart || $vavailStart < $this->start) {
$vavailStart = $this->start;
}
if (!$vavailEnd || $vavailEnd > $this->end) {
$vavailEnd = $this->end;
}
// Marking the entire time range of the VAVAILABILITY component as
// busy.
$fbData->add(
$vavailStart->getTimeStamp(),
$vavailEnd->getTimeStamp(),
$busyType
);
// Looping over the AVAILABLE components.
if (isset($vavail->AVAILABLE)) {
foreach ($vavail->AVAILABLE as $available) {
list($availStart, $availEnd) = $available->getEffectiveStartEnd();
$fbData->add(
$availStart->getTimeStamp(),
$availEnd->getTimeStamp(),
'FREE'
);
if ($available->RRULE) {
// Our favourite thing: recurrence!!
$rruleIterator = new Recur\RRuleIterator(
$available->RRULE->getValue(),
$availStart
);
$rruleIterator->fastForward($vavailStart);
$startEndDiff = $availStart->diff($availEnd);
while ($rruleIterator->valid()) {
$recurStart = $rruleIterator->current();
$recurEnd = $recurStart->add($startEndDiff);
if ($recurStart > $vavailEnd) {
// We're beyond the legal timerange.
break;
}
if ($recurEnd > $vavailEnd) {
// Truncating the end if it exceeds the
// VAVAILABILITY end.
$recurEnd = $vavailEnd;
}
$fbData->add(
$recurStart->getTimeStamp(),
$recurEnd->getTimeStamp(),
'FREE'
);
$rruleIterator->next();
}
}
}
}
}
}
/**
* This method takes an array of iCalendar objects and applies its busy
* times on fbData.
*
* @param VCalendar[] $objects
*/
protected function calculateBusy(FreeBusyData $fbData, array $objects)
{
foreach ($objects as $key => $object) {
foreach ($object->getBaseComponents() as $component) { foreach ($object->getBaseComponents() as $component) {
switch ($component->name) { switch ($component->name) {
case 'VEVENT': case 'VEVENT':
$FBTYPE = 'BUSY'; $FBTYPE = 'BUSY';
if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) { if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) {
break; break;
} }
if (isset($component->STATUS)) { if (isset($component->STATUS)) {
$status = strtoupper($component->STATUS); $status = strtoupper($component->STATUS);
if ($status==='CANCELLED') { if ('CANCELLED' === $status) {
break; break;
} }
if ($status==='TENTATIVE') { if ('TENTATIVE' === $status) {
$FBTYPE = 'BUSY-TENTATIVE'; $FBTYPE = 'BUSY-TENTATIVE';
} }
} }
$times = array(); $times = [];
if ($component->RRULE) { if ($component->RRULE) {
try {
$iterator = new EventIterator($object, (string) $component->UID, $this->timeZone);
} catch (NoInstancesException $e) {
// This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output
// entirely.
unset($this->objects[$key]);
break;
}
$iterator = new RecurrenceIterator($object, (string)$component->uid);
if ($this->start) { if ($this->start) {
$iterator->fastForward($this->start); $iterator->fastForward($this->start);
} }
$maxRecurrences = 200; $maxRecurrences = Settings::$maxRecurrences;
while ($iterator->valid() && --$maxRecurrences) { while ($iterator->valid() && --$maxRecurrences) {
$startTime = $iterator->getDTStart(); $startTime = $iterator->getDTStart();
if ($this->end && $startTime > $this->end) { if ($this->end && $startTime > $this->end) {
break; break;
} }
$times[] = array( $times[] = [
$iterator->getDTStart(), $iterator->getDTStart(),
$iterator->getDTEnd(), $iterator->getDTEnd(),
); ];
$iterator->next(); $iterator->next();
} }
} else { } else {
$startTime = $component->DTSTART->getDateTime($this->timeZone);
$startTime = $component->DTSTART->getDateTime();
if ($this->end && $startTime > $this->end) { if ($this->end && $startTime > $this->end) {
break; break;
} }
$endTime = null; $endTime = null;
if (isset($component->DTEND)) { if (isset($component->DTEND)) {
$endTime = $component->DTEND->getDateTime(); $endTime = $component->DTEND->getDateTime($this->timeZone);
} elseif (isset($component->DURATION)) { } elseif (isset($component->DURATION)) {
$duration = DateTimeParser::parseDuration((string) $component->DURATION); $duration = DateTimeParser::parseDuration((string) $component->DURATION);
$endTime = clone $startTime; $endTime = clone $startTime;
$endTime->add($duration); $endTime = $endTime->add($duration);
} elseif (!$component->DTSTART->hasTime()) { } elseif (!$component->DTSTART->hasTime()) {
$endTime = clone $startTime; $endTime = clone $startTime;
$endTime->modify('+1 day'); $endTime = $endTime->modify('+1 day');
} else { } else {
// The event had no duration (0 seconds) // The event had no duration (0 seconds)
break; break;
} }
$times[] = array($startTime, $endTime); $times[] = [$startTime, $endTime];
} }
foreach ($times as $time) { foreach ($times as $time) {
if ($this->end && $time[0] > $this->end) {
break;
}
if ($this->start && $time[1] < $this->start) {
break;
}
if ($this->end && $time[0] > $this->end) break; $fbData->add(
if ($this->start && $time[1] < $this->start) break; $time[0]->getTimeStamp(),
$time[1]->getTimeStamp(),
$busyTimes[] = array( $FBTYPE
$time[0],
$time[1],
$FBTYPE,
); );
} }
break; break;
case 'VFREEBUSY': case 'VFREEBUSY':
foreach ($component->FREEBUSY as $freebusy) { foreach ($component->FREEBUSY as $freebusy) {
$fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY'; $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY';
// Skipping intervals marked as 'free' // Skipping intervals marked as 'free'
if ($fbType==='FREE') if ('FREE' === $fbType) {
continue; continue;
}
$values = explode(',', $freebusy); $values = explode(',', $freebusy);
foreach ($values as $value) { foreach ($values as $value) {
list($startTime, $endTime) = explode('/', $value); list($startTime, $endTime) = explode('/', $value);
$startTime = DateTimeParser::parseDateTime($startTime); $startTime = DateTimeParser::parseDateTime($startTime);
if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') { if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) {
$duration = DateTimeParser::parseDuration($endTime); $duration = DateTimeParser::parseDuration($endTime);
$endTime = clone $startTime; $endTime = clone $startTime;
$endTime->add($duration); $endTime = $endTime->add($duration);
} else { } else {
$endTime = DateTimeParser::parseDateTime($endTime); $endTime = DateTimeParser::parseDateTime($endTime);
} }
if($this->start && $this->start > $endTime) continue; if ($this->start && $this->start > $endTime) {
if($this->end && $this->end < $startTime) continue; continue;
$busyTimes[] = array( }
$startTime, if ($this->end && $this->end < $startTime) {
$endTime, continue;
}
$fbData->add(
$startTime->getTimeStamp(),
$endTime->getTimeStamp(),
$fbType $fbType
); );
} }
} }
break; break;
}
}
}
}
}
} }
/**
* This method takes a FreeBusyData object and generates the VCALENDAR
* object associated with it.
*
* @return VCalendar
*/
protected function generateFreeBusyCalendar(FreeBusyData $fbData)
{
if ($this->baseObject) { if ($this->baseObject) {
$calendar = $this->baseObject; $calendar = $this->baseObject;
} else { } else {
@ -296,27 +514,36 @@ class FreeBusyGenerator {
$dtend->setDateTime($this->end); $dtend->setDateTime($this->end);
$vfreebusy->add($dtend); $vfreebusy->add($dtend);
} }
$tz = new \DateTimeZone('UTC');
$dtstamp = $calendar->createProperty('DTSTAMP'); $dtstamp = $calendar->createProperty('DTSTAMP');
$dtstamp->setDateTime(new \DateTime('now', new \DateTimeZone('UTC'))); $dtstamp->setDateTime(new DateTimeImmutable('now', $tz));
$vfreebusy->add($dtstamp); $vfreebusy->add($dtstamp);
foreach($busyTimes as $busyTime) { foreach ($fbData->getData() as $busyTime) {
$busyType = strtoupper($busyTime['type']);
$busyTime[0]->setTimeZone(new \DateTimeZone('UTC')); // Ignoring all the FREE parts, because those are already assumed.
$busyTime[1]->setTimeZone(new \DateTimeZone('UTC')); if ('FREE' === $busyType) {
continue;
}
$busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz);
$busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz);
$prop = $calendar->createProperty( $prop = $calendar->createProperty(
'FREEBUSY', 'FREEBUSY',
$busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z') $busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z')
); );
$prop['FBTYPE'] = $busyTime[2];
$vfreebusy->add($prop);
// Only setting FBTYPE if it's not BUSY, because BUSY is the
// default anyway.
if ('BUSY' !== $busyType) {
$prop['FBTYPE'] = $busyType;
}
$vfreebusy->add($prop);
} }
return $calendar; return $calendar;
} }
} }

View file

@ -0,0 +1,978 @@
<?php
namespace Sabre\VObject\ITip;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\Reader;
use Sabre\VObject\Recur\EventIterator;
/**
* The ITip\Broker class is a utility class that helps with processing
* so-called iTip messages.
*
* iTip is defined in rfc5546, stands for iCalendar Transport-Independent
* Interoperability Protocol, and describes the underlying mechanism for
* using iCalendar for scheduling for for example through email (also known as
* IMip) and CalDAV Scheduling.
*
* This class helps by:
*
* 1. Creating individual invites based on an iCalendar event for each
* attendee.
* 2. Generating invite updates based on an iCalendar update. This may result
* in new invites, updates and cancellations for attendees, if that list
* changed.
* 3. On the receiving end, it can create a local iCalendar event based on
* a received invite.
* 4. It can also process an invite update on a local event, ensuring that any
* overridden properties from attendees are retained.
* 5. It can create a accepted or declined iTip reply based on an invite.
* 6. It can process a reply from an invite and update an events attendee
* status based on a reply.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Broker
{
/**
* This setting determines whether the rules for the SCHEDULE-AGENT
* parameter should be followed.
*
* This is a parameter defined on ATTENDEE properties, introduced by RFC
* 6638. This parameter allows a caldav client to tell the server 'Don't do
* any scheduling operations'.
*
* If this setting is turned on, any attendees with SCHEDULE-AGENT set to
* CLIENT will be ignored. This is the desired behavior for a CalDAV
* server, but if you're writing an iTip application that doesn't deal with
* CalDAV, you may want to ignore this parameter.
*
* @var bool
*/
public $scheduleAgentServerRules = true;
/**
* The broker will try during 'parseEvent' figure out whether the change
* was significant.
*
* It uses a few different ways to do this. One of these ways is seeing if
* certain properties changed values. This list of specified here.
*
* This list is taken from:
* * http://tools.ietf.org/html/rfc5546#section-2.1.4
*
* @var string[]
*/
public $significantChangeProperties = [
'DTSTART',
'DTEND',
'DURATION',
'DUE',
'RRULE',
'RDATE',
'EXDATE',
'STATUS',
];
/**
* This method is used to process an incoming itip message.
*
* Examples:
*
* 1. A user is an attendee to an event. The organizer sends an updated
* meeting using a new iTip message with METHOD:REQUEST. This function
* will process the message and update the attendee's event accordingly.
*
* 2. The organizer cancelled the event using METHOD:CANCEL. We will update
* the users event to state STATUS:CANCELLED.
*
* 3. An attendee sent a reply to an invite using METHOD:REPLY. We can
* update the organizers event to update the ATTENDEE with its correct
* PARTSTAT.
*
* The $existingObject is updated in-place. If there is no existing object
* (because it's a new invite for example) a new object will be created.
*
* If an existing object does not exist, and the method was CANCEL or
* REPLY, the message effectively gets ignored, and no 'existingObject'
* will be created.
*
* The updated $existingObject is also returned from this function.
*
* If the iTip message was not supported, we will always return false.
*
* @param VCalendar $existingObject
*
* @return VCalendar|null
*/
public function processMessage(Message $itipMessage, VCalendar $existingObject = null)
{
// We only support events at the moment.
if ('VEVENT' !== $itipMessage->component) {
return false;
}
switch ($itipMessage->method) {
case 'REQUEST':
return $this->processMessageRequest($itipMessage, $existingObject);
case 'CANCEL':
return $this->processMessageCancel($itipMessage, $existingObject);
case 'REPLY':
return $this->processMessageReply($itipMessage, $existingObject);
default:
// Unsupported iTip message
return;
}
return $existingObject;
}
/**
* This function parses a VCALENDAR object and figure out if any messages
* need to be sent.
*
* A VCALENDAR object will be created from the perspective of either an
* attendee, or an organizer. You must pass a string identifying the
* current user, so we can figure out who in the list of attendees or the
* organizer we are sending this message on behalf of.
*
* It's possible to specify the current user as an array, in case the user
* has more than one identifying href (such as multiple emails).
*
* It $oldCalendar is specified, it is assumed that the operation is
* updating an existing event, which means that we need to look at the
* differences between events, and potentially send old attendees
* cancellations, and current attendees updates.
*
* If $calendar is null, but $oldCalendar is specified, we treat the
* operation as if the user has deleted an event. If the user was an
* organizer, this means that we need to send cancellation notices to
* people. If the user was an attendee, we need to make sure that the
* organizer gets the 'declined' message.
*
* @param VCalendar|string $calendar
* @param string|array $userHref
* @param VCalendar|string $oldCalendar
*
* @return array
*/
public function parseEvent($calendar, $userHref, $oldCalendar = null)
{
if ($oldCalendar) {
if (is_string($oldCalendar)) {
$oldCalendar = Reader::read($oldCalendar);
}
if (!isset($oldCalendar->VEVENT)) {
// We only support events at the moment
return [];
}
$oldEventInfo = $this->parseEventInfo($oldCalendar);
} else {
$oldEventInfo = [
'organizer' => null,
'significantChangeHash' => '',
'attendees' => [],
];
}
$userHref = (array) $userHref;
if (!is_null($calendar)) {
if (is_string($calendar)) {
$calendar = Reader::read($calendar);
}
if (!isset($calendar->VEVENT)) {
// We only support events at the moment
return [];
}
$eventInfo = $this->parseEventInfo($calendar);
if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) {
// If there were no attendees on either side of the equation,
// we don't need to do anything.
return [];
}
if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) {
// There was no organizer before or after the change.
return [];
}
$baseCalendar = $calendar;
// If the new object didn't have an organizer, the organizer
// changed the object from a scheduling object to a non-scheduling
// object. We just copy the info from the old object.
if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) {
$eventInfo['organizer'] = $oldEventInfo['organizer'];
$eventInfo['organizerName'] = $oldEventInfo['organizerName'];
}
} else {
// The calendar object got deleted, we need to process this as a
// cancellation / decline.
if (!$oldCalendar) {
// No old and no new calendar, there's no thing to do.
return [];
}
$eventInfo = $oldEventInfo;
if (in_array($eventInfo['organizer'], $userHref)) {
// This is an organizer deleting the event.
$eventInfo['attendees'] = [];
// Increasing the sequence, but only if the organizer deleted
// the event.
++$eventInfo['sequence'];
} else {
// This is an attendee deleting the event.
foreach ($eventInfo['attendees'] as $key => $attendee) {
if (in_array($attendee['href'], $userHref)) {
$eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'],
];
}
}
}
$baseCalendar = $oldCalendar;
}
if (in_array($eventInfo['organizer'], $userHref)) {
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
// process updates, not creation of new events.
foreach ($eventInfo['attendees'] as $attendee) {
if (in_array($attendee['href'], $userHref)) {
return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
}
}
}
return [];
}
/**
* Processes incoming REQUEST messages.
*
* This is message from an organizer, and is either a new event
* invite, or an update to an existing one.
*
* @param VCalendar $existingObject
*
* @return VCalendar|null
*/
protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null)
{
if (!$existingObject) {
// This is a new invite, and we're just going to copy over
// all the components from the invite.
$existingObject = new VCalendar();
foreach ($itipMessage->message->getComponents() as $component) {
$existingObject->add(clone $component);
}
} else {
// We need to update an existing object with all the new
// information. We can just remove all existing components
// and create new ones.
foreach ($existingObject->getComponents() as $component) {
$existingObject->remove($component);
}
foreach ($itipMessage->message->getComponents() as $component) {
$existingObject->add(clone $component);
}
}
return $existingObject;
}
/**
* Processes incoming CANCEL messages.
*
* This is a message from an organizer, and means that either an
* attendee got removed from an event, or an event got cancelled
* altogether.
*
* @param VCalendar $existingObject
*
* @return VCalendar|null
*/
protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null)
{
if (!$existingObject) {
// The event didn't exist in the first place, so we're just
// ignoring this message.
} else {
foreach ($existingObject->VEVENT as $vevent) {
$vevent->STATUS = 'CANCELLED';
$vevent->SEQUENCE = $itipMessage->sequence;
}
}
return $existingObject;
}
/**
* Processes incoming REPLY messages.
*
* The message is a reply. This is for example an attendee telling
* an organizer he accepted the invite, or declined it.
*
* @param VCalendar $existingObject
*
* @return VCalendar|null
*/
protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null)
{
// A reply can only be processed based on an existing object.
// If the object is not available, the reply is ignored.
if (!$existingObject) {
return;
}
$instances = [];
$requestStatus = '2.0';
// Finding all the instances the attendee replied to.
foreach ($itipMessage->message->VEVENT as $vevent) {
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
$attendee = $vevent->ATTENDEE;
$instances[$recurId] = $attendee['PARTSTAT']->getValue();
if (isset($vevent->{'REQUEST-STATUS'})) {
$requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
list($requestStatus) = explode(';', $requestStatus);
}
}
// Now we need to loop through the original organizer event, to find
// all the instances where we have a reply for.
$masterObject = null;
foreach ($existingObject->VEVENT as $vevent) {
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
if ('master' === $recurId) {
$masterObject = $vevent;
}
if (isset($instances[$recurId])) {
$attendeeFound = false;
if (isset($vevent->ATTENDEE)) {
foreach ($vevent->ATTENDEE as $attendee) {
if ($attendee->getValue() === $itipMessage->sender) {
$attendeeFound = true;
$attendee['PARTSTAT'] = $instances[$recurId];
$attendee['SCHEDULE-STATUS'] = $requestStatus;
// Un-setting the RSVP status, because we now know
// that the attendee already replied.
unset($attendee['RSVP']);
break;
}
}
}
if (!$attendeeFound) {
// Adding a new attendee. The iTip documentation calls this
// a party crasher.
$attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [
'PARTSTAT' => $instances[$recurId],
]);
if ($itipMessage->senderName) {
$attendee['CN'] = $itipMessage->senderName;
}
}
unset($instances[$recurId]);
}
}
if (!$masterObject) {
// No master object, we can't add new instances.
return;
}
// If we got replies to instances that did not exist in the
// original list, it means that new exceptions must be created.
foreach ($instances as $recurId => $partstat) {
$recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid);
$found = false;
$iterations = 1000;
do {
$newObject = $recurrenceIterator->getEventObject();
$recurrenceIterator->next();
if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) {
$found = true;
}
--$iterations;
} while ($recurrenceIterator->valid() && !$found && $iterations);
// Invalid recurrence id. Skipping this object.
if (!$found) {
continue;
}
unset(
$newObject->RRULE,
$newObject->EXDATE,
$newObject->RDATE
);
$attendeeFound = false;
if (isset($newObject->ATTENDEE)) {
foreach ($newObject->ATTENDEE as $attendee) {
if ($attendee->getValue() === $itipMessage->sender) {
$attendeeFound = true;
$attendee['PARTSTAT'] = $partstat;
break;
}
}
}
if (!$attendeeFound) {
// Adding a new attendee
$attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [
'PARTSTAT' => $partstat,
]);
if ($itipMessage->senderName) {
$attendee['CN'] = $itipMessage->senderName;
}
}
$existingObject->add($newObject);
}
return $existingObject;
}
/**
* This method is used in cases where an event got updated, and we
* potentially need to send emails to attendees to let them know of updates
* in the events.
*
* We will detect which attendees got added, which got removed and create
* specific messages for these situations.
*
* @return array
*/
protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo)
{
// Merging attendee lists.
$attendees = [];
foreach ($oldEventInfo['attendees'] as $attendee) {
$attendees[$attendee['href']] = [
'href' => $attendee['href'],
'oldInstances' => $attendee['instances'],
'newInstances' => [],
'name' => $attendee['name'],
'forceSend' => null,
];
}
foreach ($eventInfo['attendees'] as $attendee) {
if (isset($attendees[$attendee['href']])) {
$attendees[$attendee['href']]['name'] = $attendee['name'];
$attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
$attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
} else {
$attendees[$attendee['href']] = [
'href' => $attendee['href'],
'oldInstances' => [],
'newInstances' => $attendee['instances'],
'name' => $attendee['name'],
'forceSend' => $attendee['forceSend'],
];
}
}
$messages = [];
foreach ($attendees as $attendee) {
// An organizer can also be an attendee. We should not generate any
// messages for those.
if ($attendee['href'] === $eventInfo['organizer']) {
continue;
}
$message = new Message();
$message->uid = $eventInfo['uid'];
$message->component = 'VEVENT';
$message->sequence = $eventInfo['sequence'];
$message->sender = $eventInfo['organizer'];
$message->senderName = $eventInfo['organizerName'];
$message->recipient = $attendee['href'];
$message->recipientName = $attendee['name'];
// Creating the new iCalendar body.
$icalMsg = new VCalendar();
foreach ($calendar->select('VTIMEZONE') as $timezone) {
$icalMsg->add(clone $timezone);
}
if (!$attendee['newInstances']) {
// If there are no instances the attendee is a part of, it
// means the attendee was removed and we need to send him a
// CANCEL.
$message->method = 'CANCEL';
$icalMsg->METHOD = $message->method;
$event = $icalMsg->add('VEVENT', [
'UID' => $message->uid,
'SEQUENCE' => $message->sequence,
'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
]);
if (isset($calendar->VEVENT->SUMMARY)) {
$event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue());
}
$event->add(clone $calendar->VEVENT->DTSTART);
if (isset($calendar->VEVENT->DTEND)) {
$event->add(clone $calendar->VEVENT->DTEND);
} elseif (isset($calendar->VEVENT->DURATION)) {
$event->add(clone $calendar->VEVENT->DURATION);
}
$org = $event->add('ORGANIZER', $eventInfo['organizer']);
if ($eventInfo['organizerName']) {
$org['CN'] = $eventInfo['organizerName'];
}
$event->add('ATTENDEE', $attendee['href'], [
'CN' => $attendee['name'],
]);
$message->significantChange = true;
} else {
// The attendee gets the updated event body
$message->method = 'REQUEST';
$icalMsg->METHOD = $message->method;
// We need to find out that this change is significant. If it's
// not, systems may opt to not send messages.
//
// We do this based on the 'significantChangeHash' which is
// some value that changes if there's a certain set of
// properties changed in the event, or simply if there's a
// difference in instances that the attendee is invited to.
$oldAttendeeInstances = array_keys($attendee['oldInstances']);
$newAttendeeInstances = array_keys($attendee['newInstances']);
$message->significantChange =
'REQUEST' === $attendee['forceSend'] ||
count($oldAttendeeInstances) != count($newAttendeeInstances) ||
count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 ||
$oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
$currentEvent = clone $eventInfo['instances'][$instanceId];
if ('master' === $instanceId) {
// We need to find a list of events that the attendee
// is not a part of to add to the list of exceptions.
$exceptions = [];
foreach ($eventInfo['instances'] as $instanceId => $vevent) {
if (!isset($attendee['newInstances'][$instanceId])) {
$exceptions[] = $instanceId;
}
}
// If there were exceptions, we need to add it to an
// existing EXDATE property, if it exists.
if ($exceptions) {
if (isset($currentEvent->EXDATE)) {
$currentEvent->EXDATE->setParts(array_merge(
$currentEvent->EXDATE->getParts(),
$exceptions
));
} else {
$currentEvent->EXDATE = $exceptions;
}
}
// Cleaning up any scheduling information that
// shouldn't be sent along.
unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
foreach ($currentEvent->ATTENDEE as $attendee) {
unset($attendee['SCHEDULE-FORCE-SEND']);
unset($attendee['SCHEDULE-STATUS']);
// We're adding PARTSTAT=NEEDS-ACTION to ensure that
// iOS shows an "Inbox Item"
if (!isset($attendee['PARTSTAT'])) {
$attendee['PARTSTAT'] = 'NEEDS-ACTION';
}
}
}
$currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
$icalMsg->add($currentEvent);
}
}
$message->message = $icalMsg;
$messages[] = $message;
}
return $messages;
}
/**
* Parse an event update for an attendee.
*
* This function figures out if we need to send a reply to an organizer.
*
* @param string $attendee
*
* @return Message[]
*/
protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee)
{
if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) {
return [];
}
// Don't bother generating messages for events that have already been
// cancelled.
if ('CANCELLED' === $eventInfo['status']) {
return [];
}
$oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ?
$oldEventInfo['attendees'][$attendee]['instances'] :
[];
$instances = [];
foreach ($oldInstances as $instance) {
$instances[$instance['id']] = [
'id' => $instance['id'],
'oldstatus' => $instance['partstat'],
'newstatus' => null,
];
}
foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) {
if (isset($instances[$instance['id']])) {
$instances[$instance['id']]['newstatus'] = $instance['partstat'];
} else {
$instances[$instance['id']] = [
'id' => $instance['id'],
'oldstatus' => null,
'newstatus' => $instance['partstat'],
];
}
}
// We need to also look for differences in EXDATE. If there are new
// items in EXDATE, it means that an attendee deleted instances of an
// event, which means we need to send DECLINED specifically for those
// instances.
// We only need to do that though, if the master event is not declined.
if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) {
foreach ($eventInfo['exdate'] as $exDate) {
if (!in_array($exDate, $oldEventInfo['exdate'])) {
if (isset($instances[$exDate])) {
$instances[$exDate]['newstatus'] = 'DECLINED';
} else {
$instances[$exDate] = [
'id' => $exDate,
'oldstatus' => null,
'newstatus' => 'DECLINED',
];
}
}
}
}
// Gathering a few extra properties for each instance.
foreach ($instances as $recurId => $instanceInfo) {
if (isset($eventInfo['instances'][$recurId])) {
$instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART;
} else {
$instances[$recurId]['dtstart'] = $recurId;
}
}
$message = new Message();
$message->uid = $eventInfo['uid'];
$message->method = 'REPLY';
$message->component = 'VEVENT';
$message->sequence = $eventInfo['sequence'];
$message->sender = $attendee;
$message->senderName = $eventInfo['attendees'][$attendee]['name'];
$message->recipient = $eventInfo['organizer'];
$message->recipientName = $eventInfo['organizerName'];
$icalMsg = new VCalendar();
$icalMsg->METHOD = 'REPLY';
foreach ($calendar->select('VTIMEZONE') as $timezone) {
$icalMsg->add(clone $timezone);
}
$hasReply = false;
foreach ($instances as $instance) {
if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) {
// Skip
continue;
}
$event = $icalMsg->add('VEVENT', [
'UID' => $message->uid,
'SEQUENCE' => $message->sequence,
]);
$summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : '';
// Adding properties from the correct source instance
if (isset($eventInfo['instances'][$instance['id']])) {
$instanceObj = $eventInfo['instances'][$instance['id']];
$event->add(clone $instanceObj->DTSTART);
if (isset($instanceObj->DTEND)) {
$event->add(clone $instanceObj->DTEND);
} elseif (isset($instanceObj->DURATION)) {
$event->add(clone $instanceObj->DURATION);
}
if (isset($instanceObj->SUMMARY)) {
$event->add('SUMMARY', $instanceObj->SUMMARY->getValue());
} elseif ($summary) {
$event->add('SUMMARY', $summary);
}
} else {
// This branch of the code is reached, when a reply is
// generated for an instance of a recurring event, through the
// fact that the instance has disappeared by showing up in
// EXDATE
$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
// Treat is as a DATE field
if (strlen($instance['id']) <= 8) {
$event->add('DTSTART', $dt, ['VALUE' => 'DATE']);
} else {
$event->add('DTSTART', $dt);
}
if ($summary) {
$event->add('SUMMARY', $summary);
}
}
if ('master' !== $instance['id']) {
$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
// Treat is as a DATE field
if (strlen($instance['id']) <= 8) {
$event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']);
} else {
$event->add('RECURRENCE-ID', $dt);
}
}
$organizer = $event->add('ORGANIZER', $message->recipient);
if ($message->recipientName) {
$organizer['CN'] = $message->recipientName;
}
$attendee = $event->add('ATTENDEE', $message->sender, [
'PARTSTAT' => $instance['newstatus'],
]);
if ($message->senderName) {
$attendee['CN'] = $message->senderName;
}
$hasReply = true;
}
if ($hasReply) {
$message->message = $icalMsg;
return [$message];
} else {
return [];
}
}
/**
* Returns attendee information and information about instances of an
* event.
*
* Returns an array with the following keys:
*
* 1. uid
* 2. organizer
* 3. organizerName
* 4. organizerScheduleAgent
* 5. organizerForceSend
* 6. instances
* 7. attendees
* 8. sequence
* 9. exdate
* 10. timezone - strictly the timezone on which the recurrence rule is
* based on.
* 11. significantChangeHash
* 12. status
*
* @param VCalendar $calendar
*
* @return array
*/
protected function parseEventInfo(VCalendar $calendar = null)
{
$uid = null;
$organizer = null;
$organizerName = null;
$organizerForceSend = null;
$sequence = null;
$timezone = null;
$status = null;
$organizerScheduleAgent = 'SERVER';
$significantChangeHash = '';
// Now we need to collect a list of attendees, and which instances they
// are a part of.
$attendees = [];
$instances = [];
$exdate = [];
$significantChangeEventProperties = [];
foreach ($calendar->VEVENT as $vevent) {
$eventSignificantChangeHash = '';
$rrule = [];
if (is_null($uid)) {
$uid = $vevent->UID->getValue();
} else {
if ($uid !== $vevent->UID->getValue()) {
throw new ITipException('If a calendar contained more than one event, they must have the same UID.');
}
}
if (!isset($vevent->DTSTART)) {
throw new ITipException('An event MUST have a DTSTART property.');
}
if (isset($vevent->ORGANIZER)) {
if (is_null($organizer)) {
$organizer = $vevent->ORGANIZER->getNormalizedValue();
$organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null;
} else {
if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) {
throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.');
}
}
$organizerForceSend =
isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ?
strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) :
null;
$organizerScheduleAgent =
isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ?
strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) :
'SERVER';
}
if (is_null($sequence) && isset($vevent->SEQUENCE)) {
$sequence = $vevent->SEQUENCE->getValue();
}
if (isset($vevent->EXDATE)) {
foreach ($vevent->select('EXDATE') as $val) {
$exdate = array_merge($exdate, $val->getParts());
}
sort($exdate);
}
if (isset($vevent->RRULE)) {
foreach ($vevent->select('RRULE') as $rr) {
foreach ($rr->getParts() as $key => $val) {
// ignore default values (https://github.com/sabre-io/vobject/issues/126)
if ('INTERVAL' === $key && 1 == $val) {
continue;
}
if (is_array($val)) {
$val = implode(',', $val);
}
$rrule[] = "$key=$val";
}
}
sort($rrule);
}
if (isset($vevent->STATUS)) {
$status = strtoupper($vevent->STATUS->getValue());
}
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
if (is_null($timezone)) {
if ('master' === $recurId) {
$timezone = $vevent->DTSTART->getDateTime()->getTimeZone();
} else {
$timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone();
}
}
if (isset($vevent->ATTENDEE)) {
foreach ($vevent->ATTENDEE as $attendee) {
if ($this->scheduleAgentServerRules &&
isset($attendee['SCHEDULE-AGENT']) &&
'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue())
) {
continue;
}
$partStat =
isset($attendee['PARTSTAT']) ?
strtoupper($attendee['PARTSTAT']) :
'NEEDS-ACTION';
$forceSend =
isset($attendee['SCHEDULE-FORCE-SEND']) ?
strtoupper($attendee['SCHEDULE-FORCE-SEND']) :
null;
if (isset($attendees[$attendee->getNormalizedValue()])) {
$attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [
'id' => $recurId,
'partstat' => $partStat,
'forceSend' => $forceSend,
];
} else {
$attendees[$attendee->getNormalizedValue()] = [
'href' => $attendee->getNormalizedValue(),
'instances' => [
$recurId => [
'id' => $recurId,
'partstat' => $partStat,
],
],
'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null,
'forceSend' => $forceSend,
];
}
}
$instances[$recurId] = $vevent;
}
foreach ($this->significantChangeProperties as $prop) {
if (isset($vevent->$prop)) {
$propertyValues = $vevent->select($prop);
$eventSignificantChangeHash .= $prop.':';
if ('EXDATE' === $prop) {
$eventSignificantChangeHash .= implode(',', $exdate).';';
} elseif ('RRULE' === $prop) {
$eventSignificantChangeHash .= implode(',', $rrule).';';
} else {
foreach ($propertyValues as $val) {
$eventSignificantChangeHash .= $val->getValue().';';
}
}
}
}
$significantChangeEventProperties[] = $eventSignificantChangeHash;
}
asort($significantChangeEventProperties);
foreach ($significantChangeEventProperties as $eventSignificantChangeHash) {
$significantChangeHash .= $eventSignificantChangeHash;
}
$significantChangeHash = md5($significantChangeHash);
return compact(
'uid',
'organizer',
'organizerName',
'organizerScheduleAgent',
'organizerForceSend',
'instances',
'attendees',
'sequence',
'exdate',
'timezone',
'significantChangeHash',
'status'
);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Sabre\VObject\ITip;
use Exception;
/**
* This message is emitted in case of serious problems with iTip messages.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ITipException extends Exception
{
}

View file

@ -0,0 +1,136 @@
<?php
namespace Sabre\VObject\ITip;
/**
* This class represents an iTip message.
*
* A message holds all the information relevant to the message, including the
* object itself.
*
* It should for the most part be treated as immutable.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Message
{
/**
* The object's UID.
*
* @var string
*/
public $uid;
/**
* The component type, such as VEVENT.
*
* @var string
*/
public $component;
/**
* Contains the ITip method, which is something like REQUEST, REPLY or
* CANCEL.
*
* @var string
*/
public $method;
/**
* The current sequence number for the event.
*
* @var int
*/
public $sequence;
/**
* The senders' email address.
*
* Note that this does not imply that this has to be used in a From: field
* if the message is sent by email. It may also be populated in Reply-To:
* or not at all.
*
* @var string
*/
public $sender;
/**
* The name of the sender. This is often populated from a CN parameter from
* either the ORGANIZER or ATTENDEE, depending on the message.
*
* @var string|null
*/
public $senderName;
/**
* The recipient's email address.
*
* @var string
*/
public $recipient;
/**
* The name of the recipient. This is usually populated with the CN
* parameter from the ATTENDEE or ORGANIZER property, if it's available.
*
* @var string|null
*/
public $recipientName;
/**
* After the message has been delivered, this should contain a string such
* as : 1.1;Sent or 1.2;Delivered.
*
* In case of a failure, this will hold the error status code.
*
* See:
* http://tools.ietf.org/html/rfc6638#section-7.3
*
* @var string
*/
public $scheduleStatus;
/**
* The iCalendar / iTip body.
*
* @var \Sabre\VObject\Component\VCalendar
*/
public $message;
/**
* This will be set to true, if the iTip broker considers the change
* 'significant'.
*
* In practice, this means that we'll only mark it true, if for instance
* DTSTART changed. This allows systems to only send iTip messages when
* significant changes happened. This is especially useful for iMip, as
* normally a ton of messages may be generated for normal calendar use.
*
* To see the list of properties that are considered 'significant', check
* out Sabre\VObject\ITip\Broker::$significantChangeProperties.
*
* @var bool
*/
public $significantChange = true;
/**
* Returns the schedule status as a string.
*
* For example:
* 1.2
*
* @return mixed bool|string
*/
public function getScheduleStatus()
{
if (!$this->scheduleStatus) {
return false;
} else {
list($scheduleStatus) = explode(';', $this->scheduleStatus);
return $scheduleStatus;
}
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Sabre\VObject\ITip;
/**
* SameOrganizerForAllComponentsException.
*
* This exception is emitted when an event is encountered with more than one
* component (e.g.: exceptions), but the organizer is not identical in every
* component.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SameOrganizerForAllComponentsException extends ITipException
{
}

View file

@ -0,0 +1,15 @@
<?php
namespace Sabre\VObject;
/**
* This exception is thrown whenever an invalid value is found anywhere in a
* iCalendar or vCard object.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class InvalidDataException extends \Exception
{
}

View file

@ -2,20 +2,43 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use Sabre\Xml;
/** /**
* A node is the root class for every element in an iCalendar of vCard object. * A node is the root class for every element in an iCalendar of vCard object.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable { abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable, \JsonSerializable, Xml\XmlSerializable
{
/** /**
* The following constants are used by the validate() method. * The following constants are used by the validate() method.
*
* If REPAIR is set, the validator will attempt to repair any broken data
* (if possible).
*/ */
const REPAIR = 1; const REPAIR = 1;
/**
* If this option is set, the validator will operate on the vcards on the
* assumption that the vcards need to be valid for CardDAV.
*
* This means for example that the UID is required, whereas it is not for
* regular vcards.
*/
const PROFILE_CARDDAV = 2;
/**
* If this option is set, the validator will operate on iCalendar objects
* on the assumption that the vcards need to be valid for CalDAV.
*
* This means for example that calendars can only contain objects with
* identical component types and UIDs.
*/
const PROFILE_CALDAV = 4;
/** /**
* Reference to the parent object, if this is not the top object. * Reference to the parent object, if this is not the top object.
* *
@ -24,85 +47,107 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
public $parent; public $parent;
/** /**
* Iterator override * Iterator override.
* *
* @var ElementList * @var ElementList
*/ */
protected $iterator = null; protected $iterator = null;
/** /**
* The root document * The root document.
* *
* @var Component * @var Component
*/ */
protected $root; protected $root;
/** /**
* Serializes the node into a mimedir format * Serializes the node into a mimedir format.
* *
* @return string * @return string
*/ */
abstract function serialize(); abstract public function serialize();
/** /**
* This method returns an array, with the representation as it should be * This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents. * encoded in JSON. This is used to create jCard or jCal documents.
* *
* @return array * @return array
*/ */
abstract function jsonSerialize(); #[\ReturnTypeWillChange]
abstract public function jsonSerialize();
/**
* This method serializes the data into XML. This is used to create xCard or
* xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
abstract public function xmlSerialize(Xml\Writer $writer);
/**
* Call this method on a document if you're done using it.
*
* It's intended to remove all circular references, so PHP can easily clean
* it up.
*/
public function destroy()
{
$this->parent = null;
$this->root = null;
}
/* {{{ IteratorAggregator interface */ /* {{{ IteratorAggregator interface */
/** /**
* Returns the iterator for this object * Returns the iterator for this object.
* *
* @return ElementList * @return ElementList
*/ */
public function getIterator() { #[\ReturnTypeWillChange]
public function getIterator()
if (!is_null($this->iterator)) {
if (!is_null($this->iterator)) {
return $this->iterator; return $this->iterator;
}
return new ElementList(array($this)); return new ElementList([$this]);
} }
/** /**
* Sets the overridden iterator * Sets the overridden iterator.
* *
* Note that this is not actually part of the iterator interface * Note that this is not actually part of the iterator interface
*
* @param ElementList $iterator
* @return void
*/ */
public function setIterator(ElementList $iterator) { public function setIterator(ElementList $iterator)
{
$this->iterator = $iterator; $this->iterator = $iterator;
} }
/** /**
* Validates the node for correctness. * Validates the node for correctness.
* *
* The following options are supported: * The following options are supported:
* - Node::REPAIR - If something is broken, and automatic repair may * Node::REPAIR - May attempt to automatically repair the problem.
* be attempted.
* *
* An array is returned with warnings. * This method returns an array with detected problems.
* Every element has the following properties:
* *
* Every item in the array has the following properties: * * level - problem level.
* * level - (number between 1 and 3 with severity information) * * message - A human-readable string describing the issue.
* * message - (human readable message) * * node - A reference to the problematic node.
* * node - (reference to the offending node) *
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on)
* 2 - An inconsequential issue
* 3 - A severe issue.
* *
* @param int $options * @param int $options
*
* @return array * @return array
*/ */
public function validate($options = 0) { public function validate($options = 0)
{
return array(); return [];
} }
/* }}} */ /* }}} */
@ -110,35 +155,37 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
/* {{{ Countable interface */ /* {{{ Countable interface */
/** /**
* Returns the number of elements * Returns the number of elements.
* *
* @return int * @return int
*/ */
public function count() { #[\ReturnTypeWillChange]
public function count()
{
$it = $this->getIterator(); $it = $this->getIterator();
return $it->count();
return $it->count();
} }
/* }}} */ /* }}} */
/* {{{ ArrayAccess Interface */ /* {{{ ArrayAccess Interface */
/** /**
* Checks if an item exists through ArrayAccess. * Checks if an item exists through ArrayAccess.
* *
* This method just forwards the request to the inner iterator * This method just forwards the request to the inner iterator
* *
* @param int $offset * @param int $offset
*
* @return bool * @return bool
*/ */
public function offsetExists($offset) { #[\ReturnTypeWillChange]
public function offsetExists($offset)
{
$iterator = $this->getIterator(); $iterator = $this->getIterator();
return $iterator->offsetExists($offset);
return $iterator->offsetExists($offset);
} }
/** /**
@ -147,13 +194,15 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
* This method just forwards the request to the inner iterator * This method just forwards the request to the inner iterator
* *
* @param int $offset * @param int $offset
*
* @return mixed * @return mixed
*/ */
public function offsetGet($offset) { #[\ReturnTypeWillChange]
public function offsetGet($offset)
{
$iterator = $this->getIterator(); $iterator = $this->getIterator();
return $iterator->offsetGet($offset);
return $iterator->offsetGet($offset);
} }
/** /**
@ -163,10 +212,10 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
* *
* @param int $offset * @param int $offset
* @param mixed $value * @param mixed $value
* @return void
*/ */
public function offsetSet($offset,$value) { #[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
$iterator = $this->getIterator(); $iterator = $this->getIterator();
$iterator->offsetSet($offset, $value); $iterator->offsetSet($offset, $value);
@ -175,6 +224,7 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
// This method always throws an exception, so we ignore the closing // This method always throws an exception, so we ignore the closing
// brace // brace
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
/** /**
@ -183,10 +233,10 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
* This method just forwards the request to the inner iterator * This method just forwards the request to the inner iterator
* *
* @param int $offset * @param int $offset
* @return void
*/ */
public function offsetUnset($offset) { #[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
$iterator = $this->getIterator(); $iterator = $this->getIterator();
$iterator->offsetUnset($offset); $iterator->offsetUnset($offset);
@ -195,6 +245,7 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
// This method always throws an exception, so we ignore the closing // This method always throws an exception, so we ignore the closing
// brace // brace
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
/* }}} */ /* }}} */

View file

@ -0,0 +1,75 @@
<?php
namespace Sabre\VObject;
/**
* PHPUnit Assertions.
*
* This trait can be added to your unittest to make it easier to test iCalendar
* and/or vCards.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
trait PHPUnitAssertions
{
/**
* This method tests whether two vcards or icalendar objects are
* semantically identical.
*
* It supports objects being supplied as strings, streams or
* Sabre\VObject\Component instances.
*
* PRODID is removed from both objects as this is often changes and would
* just get in the way.
*
* CALSCALE will automatically get removed if it's set to GREGORIAN.
*
* Any property that has the value **ANY** will be treated as a wildcard.
*
* @param resource|string|Component $expected
* @param resource|string|Component $actual
* @param string $message
*/
public function assertVObjectEqualsVObject($expected, $actual, $message = '')
{
$getObj = function ($input) {
if (is_resource($input)) {
$input = stream_get_contents($input);
}
if (is_string($input)) {
$input = Reader::read($input);
}
if (!$input instanceof Component) {
$this->fail('Input must be a string, stream or VObject component');
}
unset($input->PRODID);
if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) {
unset($input->CALSCALE);
}
return $input;
};
$expected = $getObj($expected)->serialize();
$actual = $getObj($actual)->serialize();
// Finding wildcards in expected.
preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$actual = preg_replace(
'|^'.preg_quote($match[1], '|').':(.*)\r$|m',
$match[1].':**ANY**'."\r",
$actual
);
}
$this->assertEquals(
$expected,
$actual,
$message
);
}
}

View file

@ -2,25 +2,25 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use use ArrayIterator;
ArrayObject; use Sabre\Xml;
/** /**
* VObject Parameter * VObject Parameter.
* *
* This class represents a parameter. A parameter is always tied to a property. * This class represents a parameter. A parameter is always tied to a property.
* In the case of: * In the case of:
* DTSTART;VALUE=DATE:20101108 * DTSTART;VALUE=DATE:20101108
* VALUE=DATE would be the parameter name and value. * VALUE=DATE would be the parameter name and value.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Parameter extends Node { class Parameter extends Node
{
/** /**
* Parameter name * Parameter name.
* *
* @var string * @var string
*/ */
@ -29,14 +29,14 @@ class Parameter extends Node {
/** /**
* vCard 2.1 allows parameters to be encoded without a name. * vCard 2.1 allows parameters to be encoded without a name.
* *
* We can deduce the parameter name based on it's value. * We can deduce the parameter name based on its value.
* *
* @var bool * @var bool
*/ */
public $noName = false; public $noName = false;
/** /**
* Parameter value * Parameter value.
* *
* @var string * @var string
*/ */
@ -50,30 +50,40 @@ class Parameter extends Node {
* @param string $name * @param string $name
* @param string $value * @param string $value
*/ */
public function __construct(Document $root, $name, $value = null) { public function __construct(Document $root, $name, $value = null)
{
$this->name = strtoupper($name); $this->name = strtoupper($name);
$this->root = $root; $this->root = $root;
if (is_null($name)) { if (is_null($name)) {
$this->noName = true; $this->noName = true;
$this->name = static::guessParameterNameByValue($value); $this->name = static::guessParameterNameByValue($value);
} }
// If guessParameterNameByValue() returns an empty string
// above, we're actually dealing with a parameter that has no value.
// In that case we have to move the value to the name.
if ('' === $this->name) {
$this->noName = false;
$this->name = strtoupper($value);
} else {
$this->setValue($value); $this->setValue($value);
} }
}
/** /**
* Try to guess property name by value, can be used for vCard 2.1 nameless parameters. * Try to guess property name by value, can be used for vCard 2.1 nameless parameters.
* *
* Figuring out what the name should have been. Note that a ton of * Figuring out what the name should have been. Note that a ton of
* these are rather silly in 2013 and would probably rarely be * these are rather silly in 2014 and would probably rarely be
* used, but we like to be complete. * used, but we like to be complete.
* *
* @param string $value * @param string $value
*
* @return string * @return string
*/ */
public static function guessParameterNameByValue($value) { public static function guessParameterNameByValue($value)
{
switch (strtoupper($value)) { switch (strtoupper($value)) {
// Encodings // Encodings
case '7-BIT': case '7-BIT':
case 'QUOTED-PRINTABLE': case 'QUOTED-PRINTABLE':
@ -85,13 +95,11 @@ class Parameter extends Node {
case 'WORK': case 'WORK':
case 'HOME': case 'HOME':
case 'PREF': case 'PREF':
// Delivery Label Type // Delivery Label Type
case 'DOM': case 'DOM':
case 'INTL': case 'INTL':
case 'POSTAL': case 'POSTAL':
case 'PARCEL': case 'PARCEL':
// Telephone types // Telephone types
case 'VOICE': case 'VOICE':
case 'FAX': case 'FAX':
@ -103,7 +111,6 @@ class Parameter extends Node {
case 'CAR': case 'CAR':
case 'ISDN': case 'ISDN':
case 'VIDEO': case 'VIDEO':
// EMAIL types (lol) // EMAIL types (lol)
case 'AOL': case 'AOL':
case 'APPLELINK': case 'APPLELINK':
@ -117,7 +124,6 @@ class Parameter extends Node {
case 'PRODIGY': case 'PRODIGY':
case 'TLX': case 'TLX':
case 'X400': case 'X400':
// Photo / Logo format types // Photo / Logo format types
case 'GIF': case 'GIF':
case 'CGM': case 'CGM':
@ -133,12 +139,10 @@ class Parameter extends Node {
case 'MPEG2': case 'MPEG2':
case 'AVI': case 'AVI':
case 'QTIME': case 'QTIME':
// Sound Digital Audio Type // Sound Digital Audio Type
case 'WAVE': case 'WAVE':
case 'PCM': case 'PCM':
case 'AIFF': case 'AIFF':
// Key types // Key types
case 'X509': case 'X509':
case 'PGP': case 'PGP':
@ -166,42 +170,35 @@ class Parameter extends Node {
* 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 $value
* @return void
*/ */
public function setValue($value) { public function setValue($value)
{
$this->value = $value; $this->value = $value;
} }
/** /**
* Returns the current value * Returns the current value.
* *
* This method will always return a string, or null. If there were multiple * This method will always return a string, or null. If there were multiple
* values, it will automatically concatinate them (separated by comma). * values, it will automatically concatenate them (separated by comma).
* *
* @return string|null * @return string|null
*/ */
public function getValue() { public function getValue()
{
if (is_array($this->value)) { if (is_array($this->value)) {
return implode(',', $this->value); return implode(',', $this->value);
} else { } else {
return $this->value; return $this->value;
} }
} }
/** /**
* Sets multiple values for this parameter. * Sets multiple values for this parameter.
*
* @param array $value
* @return void
*/ */
public function setParts(array $value) { public function setParts(array $value)
{
$this->value = $value; $this->value = $value;
} }
/** /**
@ -211,35 +208,32 @@ class Parameter extends Node {
* *
* @return array * @return array
*/ */
public function getParts() { public function getParts()
{
if (is_array($this->value)) { if (is_array($this->value)) {
return $this->value; return $this->value;
} elseif (is_null($this->value)) { } elseif (is_null($this->value)) {
return array(); return [];
} else { } else {
return array($this->value); return [$this->value];
} }
} }
/** /**
* Adds a value to this parameter * Adds a value to this parameter.
* *
* If the argument is specified as an array, all items will be added to the * If the argument is specified as an array, all items will be added to the
* parameter value list. * parameter value list.
* *
* @param string|array $part * @param string|array $part
* @return void
*/ */
public function addValue($part) { public function addValue($part)
{
if (is_null($this->value)) { if (is_null($this->value)) {
$this->value = $part; $this->value = $part;
} else { } else {
$this->value = array_merge((array) $this->value, (array) $part); $this->value = array_merge((array) $this->value, (array) $part);
} }
} }
/** /**
@ -250,15 +244,15 @@ class Parameter extends Node {
* 'WORK' or 'FAX'. * 'WORK' or 'FAX'.
* *
* @param string $value * @param string $value
*
* @return bool * @return bool
*/ */
public function has($value) { public function has($value)
{
return in_array( return in_array(
strtolower($value), strtolower($value),
array_map('strtolower', (array) $this->value) array_map('strtolower', (array) $this->value)
); );
} }
/** /**
@ -266,78 +260,108 @@ class Parameter extends Node {
* *
* @return string * @return string
*/ */
public function serialize() { public function serialize()
{
$value = $this->getParts(); $value = $this->getParts();
if (count($value)===0) { if (0 === count($value)) {
return $this->name; return $this->name.'=';
} }
if ($this->root->getDocumentType() === Document::VCARD21 && $this->noName) { if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) {
return implode(';', $value); return implode(';', $value);
} }
return $this->name . '=' . array_reduce($value, function($out, $item) { return $this->name.'='.array_reduce(
$value,
if (!is_null($out)) $out.=','; function ($out, $item) {
if (!is_null($out)) {
$out .= ',';
}
// If there's no special characters in the string, we'll use the simple // If there's no special characters in the string, we'll use the simple
// format // format.
if (!preg_match('#(?: [\n":;\^,] )#x', $item)) { //
// The list of special characters is defined as:
//
// Any character except CONTROL, DQUOTE, ";", ":", ","
//
// by the iCalendar spec:
// https://tools.ietf.org/html/rfc5545#section-3.1
//
// And we add ^ to that because of:
// https://tools.ietf.org/html/rfc6868
//
// But we've found that iCal (7.0, shipped with OSX 10.9)
// severely trips on + characters not being quoted, so we
// added + as well.
if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) {
return $out.$item; return $out.$item;
} else { } else {
// Enclosing in double-quotes, and using RFC6868 for encoding any // Enclosing in double-quotes, and using RFC6868 for encoding any
// special characters // special characters
$out.='"' . strtr($item, array( $out .= '"'.strtr(
$item,
[
'^' => '^^', '^' => '^^',
"\n" => '^n', "\n" => '^n',
'"' => '^\'', '"' => '^\'',
)) . '"'; ]
).'"';
return $out; return $out;
} }
}
}); );
} }
/** /**
* This method returns an array, with the representation as it should be * This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents. * encoded in JSON. This is used to create jCard or jCal documents.
* *
* @return array * @return array
*/ */
public function jsonSerialize() { #[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->value; return $this->value;
} }
/** /**
* Called when this object is being cast to a string * This method serializes the data into XML. This is used to create xCard or
* xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
public function xmlSerialize(Xml\Writer $writer)
{
foreach (explode(',', $this->value) as $value) {
$writer->writeElement('text', $value);
}
}
/**
* Called when this object is being cast to a string.
* *
* @return string * @return string
*/ */
public function __toString() { public function __toString()
{
return $this->getValue(); return (string) $this->getValue();
} }
/** /**
* Returns the iterator for this object * Returns the iterator for this object.
* *
* @return ElementList * @return ElementList
*/ */
public function getIterator() { #[\ReturnTypeWillChange]
public function getIterator()
if (!is_null($this->iterator)) {
if (!is_null($this->iterator)) {
return $this->iterator; return $this->iterator;
return $this->iterator = new ArrayObject((array)$this->value);
} }
return $this->iterator = new ArrayIterator((array) $this->value);
}
} }

View file

@ -5,8 +5,10 @@ namespace Sabre\VObject;
/** /**
* Exception thrown by Reader if an invalid object was attempted to be parsed. * Exception thrown by Reader if an invalid object was attempted to be parsed.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class ParseException extends \Exception { } class ParseException extends \Exception
{
}

View file

@ -2,32 +2,34 @@
namespace Sabre\VObject\Parser; namespace Sabre\VObject\Parser;
use use Sabre\VObject\Component\VCalendar;
Sabre\VObject\Component\VCalendar, use Sabre\VObject\Component\VCard;
Sabre\VObject\Component\VCard, use Sabre\VObject\Document;
Sabre\VObject\ParseException, use Sabre\VObject\EofException;
Sabre\VObject\EofException; use Sabre\VObject\ParseException;
use Sabre\VObject\Property\FlatText;
use Sabre\VObject\Property\Text;
/** /**
* Json Parser. * Json Parser.
* *
* This parser parses both the jCal and jCard formats. * This parser parses both the jCal and jCard formats.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Json extends Parser { class Json extends Parser
{
/** /**
* The input data * The input data.
* *
* @var array * @var array
*/ */
protected $input; protected $input;
/** /**
* Root component * Root component.
* *
* @var Document * @var Document
*/ */
@ -42,11 +44,12 @@ class Json extends Parser {
* If either input or options are not supplied, the defaults will be used. * If either input or options are not supplied, the defaults will be used.
* *
* @param resource|string|array|null $input * @param resource|string|array|null $input
* @param int|null $options * @param int $options
* @return array *
* @return \Sabre\VObject\Document
*/ */
public function parse($input = null, $options = null) { public function parse($input = null, $options = 0)
{
if (!is_null($input)) { if (!is_null($input)) {
$this->setInput($input); $this->setInput($input);
} }
@ -54,74 +57,77 @@ class Json extends Parser {
throw new EofException('End of input stream, or no input supplied'); throw new EofException('End of input stream, or no input supplied');
} }
if (!is_null($options)) { if (0 !== $options) {
$this->options = $options; $this->options = $options;
} }
switch ($this->input[0]) { switch ($this->input[0]) {
case 'vcalendar': case 'vcalendar':
$this->root = new VCalendar(array(), false); $this->root = new VCalendar([], false);
break; break;
case 'vcard': case 'vcard':
$this->root = new VCard(array(), false); $this->root = new VCard([], false);
break; break;
default: default:
throw new ParseException('The root component must either be a vcalendar, or a vcard'); 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));
} }
if (isset($this->input[2])) foreach($this->input[2] as $comp) { if (isset($this->input[2])) {
foreach ($this->input[2] as $comp) {
$this->root->add($this->parseComponent($comp)); $this->root->add($this->parseComponent($comp));
} }
}
// Resetting the input so we can throw an feof exception the next time. // Resetting the input so we can throw an feof exception the next time.
$this->input = null; $this->input = null;
return $this->root; return $this->root;
} }
/** /**
* Parses a component * Parses a component.
* *
* @param array $jComp
* @return \Sabre\VObject\Component * @return \Sabre\VObject\Component
*/ */
public function parseComponent(array $jComp) { public function parseComponent(array $jComp)
{
// We can remove $self from PHP 5.4 onward. // We can remove $self from PHP 5.4 onward.
$self = $this; $self = $this;
$properties = array_map(function($jProp) use ($self) { $properties = array_map(
function ($jProp) use ($self) {
return $self->parseProperty($jProp); 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) { function ($jComp) use ($self) {
return $self->parseComponent($jComp); return $self->parseComponent($jComp);
}, $jComp[2]); },
$jComp[2]
} else $components = array(); );
} else {
$components = [];
}
return $this->root->createComponent( return $this->root->createComponent(
$jComp[0], $jComp[0],
array_merge($properties, $components), array_merge($properties, $components),
$defaults = false $defaults = false
); );
} }
/** /**
* Parses properties. * Parses properties.
* *
* @param array $jProp
* @return \Sabre\VObject\Property * @return \Sabre\VObject\Property
*/ */
public function parseProperty(array $jProp) { public function parseProperty(array $jProp)
{
list( list(
$propertyName, $propertyName,
$parameters, $parameters,
@ -152,29 +158,27 @@ class Json extends Parser {
// represents TEXT values. We have to normalize these here. In the // represents TEXT values. We have to normalize these here. In the
// future we can get rid of FlatText once we're allowed to break BC // future we can get rid of FlatText once we're allowed to break BC
// again. // again.
if ($defaultPropertyClass === 'Sabre\VObject\Property\FlatText') { if (FlatText::class === $defaultPropertyClass) {
$defaultPropertyClass = 'Sabre\VObject\Property\Text'; $defaultPropertyClass = Text::class;
} }
// 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 !== get_class($prop)) {
$prop["VALUE"] = $valueType; $prop['VALUE'] = $valueType;
} }
return $prop; return $prop;
} }
/** /**
* Sets the input data * Sets the input data.
* *
* @param resource|string|array $input * @param resource|string|array $input
* @return void
*/ */
public function setInput($input) { public function setInput($input)
{
if (is_resource($input)) { if (is_resource($input)) {
$input = stream_get_contents($input); $input = stream_get_contents($input);
} }
@ -182,7 +186,5 @@ class Json extends Parser {
$input = json_decode($input); $input = json_decode($input);
} }
$this->input = $input; $this->input = $input;
} }
} }

View file

@ -2,27 +2,29 @@
namespace Sabre\VObject\Parser; namespace Sabre\VObject\Parser;
use use Sabre\VObject\Component;
Sabre\VObject\ParseException, use Sabre\VObject\Component\VCalendar;
Sabre\VObject\EofException, use Sabre\VObject\Component\VCard;
Sabre\VObject\Component, use Sabre\VObject\Document;
Sabre\VObject\Property, use Sabre\VObject\EofException;
Sabre\VObject\Component\VCalendar, use Sabre\VObject\Node;
Sabre\VObject\Component\VCard; use Sabre\VObject\ParseException;
/** /**
* MimeDir parser. * MimeDir parser.
* *
* This class parses iCalendar/vCard files and returns an array. * This class parses iCalendar 2.0 and vCard 2.1, 3.0 and 4.0 files. This
* parser will return one of the following two objects from the parse method:
* *
* The array is identical to the format jCard/jCal use. * Sabre\VObject\Component\VCalendar
* Sabre\VObject\Component\VCard
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class MimeDir extends Parser { class MimeDir extends Parser
{
/** /**
* The input stream. * The input stream.
* *
@ -31,135 +33,183 @@ class MimeDir extends Parser {
protected $input; protected $input;
/** /**
* Root component * Root component.
* *
* @var Component * @var Component
*/ */
protected $root; protected $root;
/** /**
* Parses an iCalendar or vCard file * By default all input will be assumed to be UTF-8.
*
* However, both iCalendar and vCard might be encoded using different
* character sets. The character set is usually set in the mime-type.
*
* If this is the case, use setEncoding to specify that a different
* encoding will be used. If this is set, the parser will automatically
* convert all incoming data to UTF-8.
*
* @var string
*/
protected $charset = 'UTF-8';
/**
* The list of character sets we support when decoding.
*
* This would be a const expression but for now we need to support PHP 5.5
*/
protected static $SUPPORTED_CHARSETS = [
'UTF-8',
'ISO-8859-1',
'Windows-1252',
];
/**
* Parses an iCalendar or vCard file.
* *
* Pass a stream or a string. If null is parsed, the existing buffer is * Pass a stream or a string. If null is parsed, the existing buffer is
* used. * used.
* *
* @param string|resource|null $input * @param string|resource|null $input
* @param int|null $options * @param int $options
* @return array *
* @return \Sabre\VObject\Document
*/ */
public function parse($input = null, $options = null) { public function parse($input = null, $options = 0)
{
$this->root = null; $this->root = null;
if (!is_null($input)) { if (!is_null($input)) {
$this->setInput($input); $this->setInput($input);
} }
if (!is_null($options)) $this->options = $options; if (0 !== $options) {
$this->options = $options;
}
$this->parseDocument(); $this->parseDocument();
return $this->root; return $this->root;
}
/**
* By default all input will be assumed to be UTF-8.
*
* However, both iCalendar and vCard might be encoded using different
* character sets. The character set is usually set in the mime-type.
*
* If this is the case, use setEncoding to specify that a different
* encoding will be used. If this is set, the parser will automatically
* convert all incoming data to UTF-8.
*
* @param string $charset
*/
public function setCharset($charset)
{
if (!in_array($charset, self::$SUPPORTED_CHARSETS)) {
throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')');
}
$this->charset = $charset;
} }
/** /**
* Sets the input buffer. Must be a string or stream. * Sets the input buffer. Must be a string or stream.
* *
* @param resource|string $input * @param resource|string $input
* @return void
*/ */
public function setInput($input) { public function setInput($input)
{
// Resetting the parser // Resetting the parser
$this->lineIndex = 0; $this->lineIndex = 0;
$this->startLine = 0; $this->startLine = 0;
if (is_string($input)) { if (is_string($input)) {
// Convering to a stream. // Converting to a stream.
$stream = fopen('php://temp', 'r+'); $stream = fopen('php://temp', 'r+');
fwrite($stream, $input); fwrite($stream, $input);
rewind($stream); rewind($stream);
$this->input = $stream; $this->input = $stream;
} else { } elseif (is_resource($input)) {
$this->input = $input; $this->input = $input;
} else {
throw new \InvalidArgumentException('This parser can only read from strings or streams.');
} }
} }
/** /**
* Parses an entire document. * Parses an entire document.
*
* @return void
*/ */
protected function parseDocument() { protected function parseDocument()
{
$line = $this->readLine(); $line = $this->readLine();
// BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
// It's 0xEF 0xBB 0xBF in UTF-8 hex.
if (3 <= strlen($line)
&& 0xef === ord($line[0])
&& 0xbb === ord($line[1])
&& 0xbf === ord($line[2])) {
$line = substr($line, 3);
}
switch (strtoupper($line)) { switch (strtoupper($line)) {
case 'BEGIN:VCALENDAR': case 'BEGIN:VCALENDAR':
$class = isset(VCalendar::$componentMap['VCALENDAR']) $class = VCalendar::$componentMap['VCALENDAR'];
? VCalendar::$componentMap[$name]
: 'Sabre\\VObject\\Component\\VCalendar';
break; break;
case 'BEGIN:VCARD': case 'BEGIN:VCARD':
$class = isset(VCard::$componentMap['VCARD']) $class = VCard::$componentMap['VCARD'];
? VCard::$componentMap['VCARD']
: 'Sabre\\VObject\\Component\\VCard';
break; break;
default: default:
throw new ParseException('This parser only supports VCARD and VCALENDAR files'); throw new ParseException('This parser only supports VCARD and VCALENDAR files');
} }
$this->root = new $class(array(), false); $this->root = new $class([], false);
while (true) { while (true) {
// Reading until we hit END: // Reading until we hit END:
$line = $this->readLine(); $line = $this->readLine();
if (strtoupper(substr($line,0,4)) === 'END:') { if ('END:' === strtoupper(substr($line, 0, 4))) {
break; break;
} }
$result = $this->parseLine($line); $result = $this->parseLine($line);
if ($result) { if ($result) {
$this->root->add($result); $this->root->add($result);
} }
} }
$name = strtoupper(substr($line, 4)); $name = strtoupper(substr($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.'"');
} }
} }
/** /**
* Parses a line, and if it hits a component, it will also attempt to parse * Parses a line, and if it hits a component, it will also attempt to parse
* the entire component * the entire component.
* *
* @param string $line Unfolded line * @param string $line Unfolded line
*
* @return Node * @return Node
*/ */
protected function parseLine($line) { protected function parseLine($line)
{
// Start of a new component // Start of a new component
if (strtoupper(substr($line, 0, 6)) === 'BEGIN:') { if ('BEGIN:' === strtoupper(substr($line, 0, 6))) {
if (substr($line, 6) === $this->root->name) {
$component = $this->root->createComponent(substr($line,6), array(), false); throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name);
}
$component = $this->root->createComponent(substr($line, 6), [], false);
while (true) { while (true) {
// Reading until we hit END: // Reading until we hit END:
$line = $this->readLine(); $line = $this->readLine();
if (strtoupper(substr($line,0,4)) === 'END:') { if ('END:' === strtoupper(substr($line, 0, 4))) {
break; break;
} }
$result = $this->parseLine($line); $result = $this->parseLine($line);
if ($result) { if ($result) {
$component->add($result); $component->add($result);
} }
} }
$name = strtoupper(substr($line, 4)); $name = strtoupper(substr($line, 4));
@ -168,19 +218,16 @@ class MimeDir extends Parser {
} }
return $component; return $component;
} else { } else {
// Property reader // Property reader
$property = $this->readProperty($line); $property = $this->readProperty($line);
if (!$property) { if (!$property) {
// Ignored line // Ignored line
return false; return false;
} }
return $property; return $property;
} }
} }
/** /**
@ -189,7 +236,7 @@ class MimeDir extends Parser {
* *
* If that was not the case, we store it here. * If that was not the case, we store it here.
* *
* @var null|string * @var string|null
*/ */
protected $lineBuffer; protected $lineBuffer;
@ -219,22 +266,29 @@ class MimeDir extends Parser {
* This method strips any newlines and also takes care of unfolding. * This method strips any newlines and also takes care of unfolding.
* *
* @throws \Sabre\VObject\EofException * @throws \Sabre\VObject\EofException
*
* @return string * @return string
*/ */
protected function readLine() { protected function readLine()
{
if (!is_null($this->lineBuffer)) { if (!\is_null($this->lineBuffer)) {
$rawLine = $this->lineBuffer; $rawLine = $this->lineBuffer;
$this->lineBuffer = null; $this->lineBuffer = null;
} else { } else {
do { do {
$rawLine = fgets($this->input); $eof = \feof($this->input);
if ($rawLine === false && feof($this->input)) {
$rawLine = \fgets($this->input);
if ($eof || (\feof($this->input) && false === $rawLine)) {
throw new EofException('End of document reached prematurely'); throw new EofException('End of document reached prematurely');
} }
$rawLine = rtrim($rawLine, "\r\n"); if (false === $rawLine) {
} while ($rawLine === ''); // Skipping empty lines throw new ParseException('Error reading from input stream');
$this->lineIndex++; }
$rawLine = \rtrim($rawLine, "\r\n");
} while ('' === $rawLine); // Skipping empty lines
++$this->lineIndex;
} }
$line = $rawLine; $line = $rawLine;
@ -242,33 +296,30 @@ class MimeDir extends Parser {
// Looking ahead for folded lines. // Looking ahead for folded lines.
while (true) { while (true) {
$nextLine = \rtrim(\fgets($this->input), "\r\n");
$nextLine = rtrim(fgets($this->input), "\r\n"); ++$this->lineIndex;
$this->lineIndex++;
if (!$nextLine) { if (!$nextLine) {
break; break;
} }
if ($nextLine[0] === "\t" || $nextLine[0] === " ") { if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) {
$line .= substr($nextLine, 1); $curLine = \substr($nextLine, 1);
$rawLine .= "\n " . substr($nextLine, 1); $line .= $curLine;
$rawLine .= "\n ".$curLine;
} else { } else {
$this->lineBuffer = $nextLine; $this->lineBuffer = $nextLine;
break; break;
} }
} }
$this->rawLine = $rawLine; $this->rawLine = $rawLine;
return $line;
return $line;
} }
/** /**
* Reads a property or component from a line. * Reads a property or component from a line.
*
* @return void
*/ */
protected function readProperty($line) { protected function readProperty($line)
{
if ($this->options & self::OPTION_FORGIVING) { if ($this->options & self::OPTION_FORGIVING) {
$propNameToken = 'A-Z0-9\-\._\\/'; $propNameToken = 'A-Z0-9\-\._\\/';
} else { } else {
@ -282,7 +333,7 @@ class MimeDir extends Parser {
$regex = "/ $regex = "/
^(?P<name> [$propNameToken]+ ) (?=[;:]) # property name ^(?P<name> [$propNameToken]+ ) (?=[;:]) # property name
| |
(?<=:)(?P<propValue> .*)$ # property value (?<=:)(?P<propValue> .+)$ # property value
| |
;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name ;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name
| |
@ -292,18 +343,18 @@ class MimeDir extends Parser {
) (?=[;:,]) ) (?=[;:,])
/xi"; /xi";
//echo $regex, "\n"; die(); //echo $regex, "\n"; exit();
preg_match_all($regex, $line, $matches, PREG_SET_ORDER); preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
$property = array( $property = [
'name' => null, 'name' => null,
'parameters' => array(), 'parameters' => [],
'value' => null 'value' => null,
); ];
$lastParam = null; $lastParam = null;
/** /*
* Looping through all the tokens. * Looping through all the tokens.
* *
* Note that we are looping through them in reverse order, because if a * Note that we are looping through them in reverse order, because if a
@ -311,9 +362,8 @@ class MimeDir extends Parser {
* in the result. * in the result.
*/ */
foreach ($matches as $match) { foreach ($matches as $match) {
if (isset($match['paramValue'])) { if (isset($match['paramValue'])) {
if ($match['paramValue'] && $match['paramValue'][0] === '"') { if ($match['paramValue'] && '"' === $match['paramValue'][0]) {
$value = substr($match['paramValue'], 1, -1); $value = substr($match['paramValue'], 1, -1);
} else { } else {
$value = $match['paramValue']; $value = $match['paramValue'];
@ -321,15 +371,18 @@ class MimeDir extends Parser {
$value = $this->unescapeParam($value); $value = $this->unescapeParam($value);
if (is_null($lastParam)) {
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
}
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])) {
$property['parameters'][$lastParam][] = $value; $property['parameters'][$lastParam][] = $value;
} else { } else {
$property['parameters'][$lastParam] = array( $property['parameters'][$lastParam] = [
$property['parameters'][$lastParam], $property['parameters'][$lastParam],
$value $value,
); ];
} }
continue; continue;
} }
@ -352,10 +405,12 @@ class MimeDir extends Parser {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
throw new \LogicException('This code should not be reachable'); throw new \LogicException('This code should not be reachable');
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
if (is_null($property['value']) || !$property['name']) { if (is_null($property['value'])) {
$property['value'] = '';
}
if (!$property['name']) {
if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
return false; return false;
} }
@ -363,12 +418,12 @@ class MimeDir extends Parser {
} }
// vCard 2.1 states that parameters may appear without a name, and only // vCard 2.1 states that parameters may appear without a name, and only
// a value. We can deduce the value based on it's name. // a value. We can deduce the value based on its name.
// //
// Our parser will get those as parameters without a value instead, so // Our parser will get those as parameters without a value instead, so
// we're filtering these parameters out first. // we're filtering these parameters out first.
$namedParameters = array(); $namedParameters = [];
$namelessParameters = array(); $namelessParameters = [];
foreach ($property['parameters'] as $name => $value) { foreach ($property['parameters'] as $name => $value) {
if (!is_null($value)) { if (!is_null($value)) {
@ -384,14 +439,30 @@ class MimeDir extends Parser {
$propObj->add(null, $namelessParameter); $propObj->add(null, $namelessParameter);
} }
if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') { if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) {
$propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
} else { } else {
$charset = $this->charset;
if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) {
// vCard 2.1 allows the character set to be specified per property.
$charset = (string) $propObj['CHARSET'];
}
switch (strtolower($charset)) {
case 'utf-8':
break;
case 'iso-8859-1':
$property['value'] = utf8_encode($property['value']);
break;
case 'windows-1252':
$property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset);
break;
default:
throw new ParseException('Unsupported CHARSET: '.$propObj['CHARSET']);
}
$propObj->setRawMimeDirValue($property['value']); $propObj->setRawMimeDirValue($property['value']);
} }
return $propObj; return $propObj;
} }
/** /**
@ -409,7 +480,7 @@ class MimeDir extends Parser {
* vCard 3.0 says: * vCard 3.0 says:
* * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
* escaped, all time time. * escaped, all time time.
* * Comma's are used for delimeters in multiple values * * Comma's are used for delimiters in multiple values
* * (rfc2426) Adds to to this that the semi-colon MUST also be escaped, * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
* as in some properties semi-colon is used for separators. * as in some properties semi-colon is used for separators.
* * Properties using semi-colons: N, ADR, GEO, ORG * * Properties using semi-colons: N, ADR, GEO, ORG
@ -447,16 +518,17 @@ class MimeDir extends Parser {
* *
* Now for the parameters * Now for the parameters
* *
* If delimiter is not set (null) this method will just return a string. * If delimiter is not set (empty string) this method will just return a string.
* If it's a comma or a semi-colon the string will be split on those * If it's a comma or a semi-colon the string will be split on those
* characters, and always return an array. * characters, and always return an array.
* *
* @param string $input * @param string $input
* @param string $delimiter * @param string $delimiter
*
* @return string|string[] * @return string|string[]
*/ */
static public function unescapeValue($input, $delimiter = ';') { public static function unescapeValue($input, $delimiter = ';')
{
$regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )'; $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
if ($delimiter) { if ($delimiter) {
$regex .= ' | ('.$delimiter.')'; $regex .= ' | ('.$delimiter.')';
@ -465,11 +537,10 @@ class MimeDir extends Parser {
$matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$resultArray = array(); $resultArray = [];
$result = ''; $result = '';
foreach ($matches as $match) { foreach ($matches as $match) {
switch ($match) { switch ($match) {
case '\\\\': case '\\\\':
$result .= '\\'; $result .= '\\';
@ -491,14 +562,12 @@ class MimeDir extends Parser {
default: default:
$result .= $match; $result .= $match;
break; break;
} }
} }
$resultArray[] = $result; $resultArray[] = $result;
return $delimiter ? $resultArray : $result;
return $delimiter ? $resultArray : $result;
} }
/** /**
@ -532,12 +601,13 @@ class MimeDir extends Parser {
* * " is encoded as ^' * * " is encoded as ^'
* *
* @param string $input * @param string $input
* @return void
*/ */
private function unescapeParam($input) { private function unescapeParam($input)
{
return return
preg_replace_callback('#(\^(\^|n|\'))#',function($matches) { preg_replace_callback(
'#(\^(\^|n|\'))#',
function ($matches) {
switch ($matches[2]) { switch ($matches[2]) {
case 'n': case 'n':
return "\n"; return "\n";
@ -549,8 +619,9 @@ class MimeDir extends Parser {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
}, $input); },
$input
);
} }
/** /**
@ -563,8 +634,8 @@ class MimeDir extends Parser {
* *
* @return string * @return string
*/ */
private function extractQuotedPrintableValue() { private function extractQuotedPrintableValue()
{
// We need to parse the raw line again to get the start of the value. // We need to parse the raw line again to get the start of the value.
// //
// We are basically looking for the first colon (:), but we need to // We are basically looking for the first colon (:), but we need to
@ -587,7 +658,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) === '=') { while ('=' === substr($value, -1) && $this->lineBuffer) {
// Reading the line // Reading the line
$this->readLine(); $this->readLine();
// Grabbing the raw form // Grabbing the raw form
@ -596,7 +667,5 @@ class MimeDir extends Parser {
} }
return $value; return $value;
} }
} }

View file

@ -7,12 +7,12 @@ namespace Sabre\VObject\Parser;
* *
* This class serves as a base-class for the different parsers. * This class serves as a base-class for the different parsers.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
abstract class Parser { abstract class Parser
{
/** /**
* Turning on this option makes the parser more forgiving. * Turning on this option makes the parser more forgiving.
* *
@ -29,7 +29,7 @@ abstract class Parser {
const OPTION_IGNORE_INVALID_LINES = 2; const OPTION_IGNORE_INVALID_LINES = 2;
/** /**
* Bitmask of parser options * Bitmask of parser options.
* *
* @var int * @var int
*/ */
@ -41,11 +41,10 @@ abstract class Parser {
* Optionally, it's possible to parse the input stream here. * Optionally, it's possible to parse the input stream here.
* *
* @param mixed $input * @param mixed $input
* @param int $options Any parser options (OPTION constants). * @param int $options any parser options (OPTION constants)
* @return void
*/ */
public function __construct($input = null, $options = 0) { public function __construct($input = null, $options = 0)
{
if (!is_null($input)) { if (!is_null($input)) {
$this->setInput($input); $this->setInput($input);
} }
@ -61,17 +60,16 @@ abstract class Parser {
* If either input or options are not supplied, the defaults will be used. * If either input or options are not supplied, the defaults will be used.
* *
* @param mixed $input * @param mixed $input
* @param int|null $options * @param int $options
*
* @return array * @return array
*/ */
abstract public function parse($input = null, $options = null); abstract public function parse($input = null, $options = 0);
/** /**
* Sets the input data * Sets the input data.
* *
* @param mixed $input * @param mixed $input
* @return void
*/ */
abstract public function setInput($input); abstract public function setInput($input);
} }

View file

@ -0,0 +1,377 @@
<?php
namespace Sabre\VObject\Parser;
use Sabre\VObject\Component;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\EofException;
use Sabre\VObject\ParseException;
use Sabre\Xml as SabreXml;
/**
* XML Parser.
*
* This parser parses both the xCal and xCard formats.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Ivan Enderlin
* @license http://sabre.io/license/ Modified BSD License
*/
class XML extends Parser
{
const XCAL_NAMESPACE = 'urn:ietf:params:xml:ns:icalendar-2.0';
const XCARD_NAMESPACE = 'urn:ietf:params:xml:ns:vcard-4.0';
/**
* The input data.
*
* @var array
*/
protected $input;
/**
* A pointer/reference to the input.
*
* @var array
*/
private $pointer;
/**
* Document, root component.
*
* @var \Sabre\VObject\Document
*/
protected $root;
/**
* Creates the parser.
*
* Optionally, it's possible to parse the input stream here.
*
* @param mixed $input
* @param int $options any parser options (OPTION constants)
*/
public function __construct($input = null, $options = 0)
{
if (0 === $options) {
$options = parent::OPTION_FORGIVING;
}
parent::__construct($input, $options);
}
/**
* Parse xCal or xCard.
*
* @param resource|string $input
* @param int $options
*
* @throws \Exception
*
* @return \Sabre\VObject\Document
*/
public function parse($input = null, $options = 0)
{
if (!is_null($input)) {
$this->setInput($input);
}
if (0 !== $options) {
$this->options = $options;
}
if (is_null($this->input)) {
throw new EofException('End of input stream, or no input supplied');
}
switch ($this->input['name']) {
case '{'.self::XCAL_NAMESPACE.'}icalendar':
$this->root = new VCalendar([], false);
$this->pointer = &$this->input['value'][0];
$this->parseVCalendarComponents($this->root);
break;
case '{'.self::XCARD_NAMESPACE.'}vcards':
foreach ($this->input['value'] as &$vCard) {
$this->root = new VCard(['version' => '4.0'], false);
$this->pointer = &$vCard;
$this->parseVCardComponents($this->root);
// We just parse the first <vcard /> element.
break;
}
break;
default:
throw new ParseException('Unsupported XML standard');
}
return $this->root;
}
/**
* Parse a xCalendar component.
*/
protected function parseVCalendarComponents(Component $parentComponent)
{
foreach ($this->pointer['value'] ?: [] as $children) {
switch (static::getTagName($children['name'])) {
case 'properties':
$this->pointer = &$children['value'];
$this->parseProperties($parentComponent);
break;
case 'components':
$this->pointer = &$children;
$this->parseComponent($parentComponent);
break;
}
}
}
/**
* Parse a xCard component.
*/
protected function parseVCardComponents(Component $parentComponent)
{
$this->pointer = &$this->pointer['value'];
$this->parseProperties($parentComponent);
}
/**
* Parse xCalendar and xCard properties.
*
* @param string $propertyNamePrefix
*/
protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '')
{
foreach ($this->pointer ?: [] as $xmlProperty) {
list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']);
$propertyName = $tagName;
$propertyValue = [];
$propertyParameters = [];
$propertyType = 'text';
// A property which is not part of the standard.
if (self::XCAL_NAMESPACE !== $namespace
&& self::XCARD_NAMESPACE !== $namespace) {
$propertyName = 'xml';
$value = '<'.$tagName.' xmlns="'.$namespace.'"';
foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) {
$value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"';
}
$value .= '>'.$xmlProperty['value'].'</'.$tagName.'>';
$propertyValue = [$value];
$this->createProperty(
$parentComponent,
$propertyName,
$propertyParameters,
$propertyType,
$propertyValue
);
continue;
}
// xCard group.
if ('group' === $propertyName) {
if (!isset($xmlProperty['attributes']['name'])) {
continue;
}
$this->pointer = &$xmlProperty['value'];
$this->parseProperties(
$parentComponent,
strtoupper($xmlProperty['attributes']['name']).'.'
);
continue;
}
// Collect parameters.
foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) {
if (!is_array($xmlPropertyChild)
|| 'parameters' !== static::getTagName($xmlPropertyChild['name'])) {
continue;
}
$xmlParameters = $xmlPropertyChild['value'];
foreach ($xmlParameters as $xmlParameter) {
$propertyParameterValues = [];
foreach ($xmlParameter['value'] as $xmlParameterValues) {
$propertyParameterValues[] = $xmlParameterValues['value'];
}
$propertyParameters[static::getTagName($xmlParameter['name'])]
= implode(',', $propertyParameterValues);
}
array_splice($xmlProperty['value'], $i, 1);
}
$propertyNameExtended = ($this->root instanceof VCalendar
? 'xcal'
: 'xcard').':'.$propertyName;
switch ($propertyNameExtended) {
case 'xcal:geo':
$propertyType = 'float';
$propertyValue['latitude'] = 0;
$propertyValue['longitude'] = 0;
foreach ($xmlProperty['value'] as $xmlRequestChild) {
$propertyValue[static::getTagName($xmlRequestChild['name'])]
= $xmlRequestChild['value'];
}
break;
case 'xcal:request-status':
$propertyType = 'text';
foreach ($xmlProperty['value'] as $xmlRequestChild) {
$propertyValue[static::getTagName($xmlRequestChild['name'])]
= $xmlRequestChild['value'];
}
break;
case 'xcal:freebusy':
$propertyType = 'freebusy';
// We don't break because we only want to set
// another property type.
// no break
case 'xcal:categories':
case 'xcal:resources':
case 'xcal:exdate':
foreach ($xmlProperty['value'] as $specialChild) {
$propertyValue[static::getTagName($specialChild['name'])]
= $specialChild['value'];
}
break;
case 'xcal:rdate':
$propertyType = 'date-time';
foreach ($xmlProperty['value'] as $specialChild) {
$tagName = static::getTagName($specialChild['name']);
if ('period' === $tagName) {
$propertyParameters['value'] = 'PERIOD';
$propertyValue[] = implode('/', $specialChild['value']);
} else {
$propertyValue[] = $specialChild['value'];
}
}
break;
default:
$propertyType = static::getTagName($xmlProperty['value'][0]['name']);
foreach ($xmlProperty['value'] as $value) {
$propertyValue[] = $value['value'];
}
if ('date' === $propertyType) {
$propertyParameters['value'] = 'DATE';
}
break;
}
$this->createProperty(
$parentComponent,
$propertyNamePrefix.$propertyName,
$propertyParameters,
$propertyType,
$propertyValue
);
}
}
/**
* Parse a component.
*/
protected function parseComponent(Component $parentComponent)
{
$components = $this->pointer['value'] ?: [];
foreach ($components as $component) {
$componentName = static::getTagName($component['name']);
$currentComponent = $this->root->createComponent(
$componentName,
null,
false
);
$this->pointer = &$component;
$this->parseVCalendarComponents($currentComponent);
$parentComponent->add($currentComponent);
}
}
/**
* Create a property.
*
* @param string $name
* @param array $parameters
* @param string $type
* @param mixed $value
*/
protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value)
{
$property = $this->root->createProperty(
$name,
null,
$parameters,
$type
);
$parentComponent->add($property);
$property->setXmlValue($value);
}
/**
* Sets the input data.
*
* @param resource|string $input
*/
public function setInput($input)
{
if (is_resource($input)) {
$input = stream_get_contents($input);
}
if (is_string($input)) {
$reader = new SabreXml\Reader();
$reader->elementMap['{'.self::XCAL_NAMESPACE.'}period']
= XML\Element\KeyValue::class;
$reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur']
= XML\Element\KeyValue::class;
$reader->xml($input);
$input = $reader->parse();
}
$this->input = $input;
}
/**
* Get tag name from a Clark notation.
*
* @param string $clarkedTagName
*
* @return string
*/
protected static function getTagName($clarkedTagName)
{
list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName);
return $tagName;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Sabre\VObject\Parser\XML\Element;
use Sabre\Xml as SabreXml;
/**
* Our own sabre/xml key-value element.
*
* It just removes the clark notation.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Ivan Enderlin
* @license http://sabre.io/license/ Modified BSD License
*/
class KeyValue extends SabreXml\Element\KeyValue
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* Important note 2: You are responsible for advancing the reader to the
* next element. Not doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @param XML\Reader $reader
*
* @return mixed
*/
public static function xmlDeserialize(SabreXml\Reader $reader)
{
// If there's no children, we don't do anything.
if ($reader->isEmptyElement) {
$reader->next();
return [];
}
$values = [];
$reader->read();
do {
if (SabreXml\Reader::ELEMENT === $reader->nodeType) {
$name = $reader->localName;
$values[$name] = $reader->parseCurrentElement()['value'];
} else {
$reader->read();
}
} while (SabreXml\Reader::END_ELEMENT !== $reader->nodeType);
$reader->read();
return $values;
}
}

View file

@ -2,18 +2,20 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use Sabre\Xml;
/** /**
* Property * Property.
* *
* A property is always in a KEY:VALUE structure, and may optionally contain * A property is always in a KEY:VALUE structure, and may optionally contain
* parameters. * parameters.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
abstract class Property extends Node { abstract class Property extends Node
{
/** /**
* Property name. * Property name.
* *
@ -33,14 +35,14 @@ abstract class Property extends Node {
public $group; public $group;
/** /**
* List of parameters * List of parameters.
* *
* @var array * @var array
*/ */
public $parameters = array(); public $parameters = [];
/** /**
* Current value * Current value.
* *
* @var mixed * @var mixed
*/ */
@ -50,7 +52,7 @@ abstract class Property extends Node {
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = ';'; public $delimiter = ';';
@ -64,23 +66,21 @@ abstract class Property extends Node {
* @param string|array|null $value * @param string|array|null $value
* @param array $parameters List of parameters * @param array $parameters List of parameters
* @param string $group The vcard property group * @param string $group The vcard property group
* @return void
*/ */
public function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) { public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null)
{
$this->name = $name; $this->name = $name;
$this->group = $group; $this->group = $group;
$this->root = $root; $this->root = $root;
if (!is_null($value)) {
$this->setValue($value);
}
foreach ($parameters as $k => $v) { foreach ($parameters as $k => $v) {
$this->add($k, $v); $this->add($k, $v);
} }
if (!is_null($value)) {
$this->setValue($value);
}
} }
/** /**
@ -89,12 +89,10 @@ abstract class Property extends Node {
* 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 $value
* @return void
*/ */
public function setValue($value) { public function setValue($value)
{
$this->value = $value; $this->value = $value;
} }
/** /**
@ -108,32 +106,27 @@ abstract class Property extends Node {
* *
* @return string * @return string
*/ */
public function getValue() { public function getValue()
{
if (is_array($this->value)) { if (is_array($this->value)) {
if (count($this->value)==0) { if (0 == count($this->value)) {
return null; return;
} elseif (count($this->value)===1) { } elseif (1 === count($this->value)) {
return $this->value[0]; return $this->value[0];
} else { } else {
return $this->getRawMimeDirValue($this->value); return $this->getRawMimeDirValue();
} }
} else { } else {
return $this->value; return $this->value;
} }
} }
/** /**
* Sets a multi-valued property. * Sets a multi-valued property.
*
* @param array $parts
* @return void
*/ */
public function setParts(array $parts) { public function setParts(array $parts)
{
$this->value = $parts; $this->value = $parts;
} }
/** /**
@ -144,40 +137,38 @@ abstract class Property extends Node {
* *
* @return array * @return array
*/ */
public function getParts() { public function getParts()
{
if (is_null($this->value)) { if (is_null($this->value)) {
return array(); return [];
} elseif (is_array($this->value)) { } elseif (is_array($this->value)) {
return $this->value; return $this->value;
} else { } else {
return array($this->value); return [$this->value];
} }
} }
/** /**
* Adds a new parameter, and returns the new item. * Adds a new parameter.
* *
* If a parameter with same name already existed, the values will be * If a parameter with same name already existed, the values will be
* combined. * combined.
* If nameless parameter is added, we try to guess it's name. * If nameless parameter is added, we try to guess its name.
* *
* @param string $name * @param string $name
* @param string|null|array $value * @param string|array|null $value
* @return Node
*/ */
public function add($name, $value = null) { public function add($name, $value = null)
{
$noName = false; $noName = false;
if ($name === null) { if (null === $name) {
$name = Parameter::guessParameterNameByValue($value); $name = Parameter::guessParameterNameByValue($value);
$noName = true; $noName = true;
} }
if (isset($this->parameters[strtoupper($name)])) { if (isset($this->parameters[strtoupper($name)])) {
$this->parameters[strtoupper($name)]->addValue($value); $this->parameters[strtoupper($name)]->addValue($value);
} } else {
else {
$param = new Parameter($this->root, $name, $value); $param = new Parameter($this->root, $name, $value);
$param->noName = $noName; $param->noName = $noName;
$this->parameters[$param->name] = $param; $this->parameters[$param->name] = $param;
@ -185,14 +176,13 @@ abstract class Property extends Node {
} }
/** /**
* Returns an iterable list of children * Returns an iterable list of children.
* *
* @return array * @return array
*/ */
public function parameters() { public function parameters()
{
return $this->parameters; return $this->parameters;
} }
/** /**
@ -212,7 +202,6 @@ abstract class Property extends Node {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
abstract public function setRawMimeDirValue($val); abstract public function setRawMimeDirValue($val);
@ -228,78 +217,72 @@ abstract class Property extends Node {
* *
* @return string * @return string
*/ */
public function serialize() { public function serialize()
{
$str = $this->name; $str = $this->name;
if ($this->group) $str = $this->group . '.' . $this->name; if ($this->group) {
$str = $this->group.'.'.$this->name;
foreach($this->parameters as $param) { }
foreach ($this->parameters() as $param) {
$str .= ';'.$param->serialize(); $str .= ';'.$param->serialize();
} }
$str .= ':'.$this->getRawMimeDirValue(); $str .= ':'.$this->getRawMimeDirValue();
$out = ''; $str = \preg_replace(
while(strlen($str)>0) { '/(
if (strlen($str)>75) { (?:^.)? # 1 additional byte in first line because of missing single space (see next line)
$out.= mb_strcut($str,0,75,'utf-8') . "\r\n"; .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
$str = ' ' . mb_strcut($str,75,strlen($str),'utf-8'); (?![\x80-\xbf]) # prevent splitting multibyte characters
} else { )/x',
$out.=$str . "\r\n"; "$1\r\n ",
$str=''; $str
break; );
}
}
return $out;
// remove single space after last CRLF
return \substr($str, 0, -1);
} }
/** /**
* Returns the value, in the format it should be encoded for json. * Returns the value, in the format it should be encoded for JSON.
* *
* This method must always return an array. * This method must always return an array.
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
return $this->getParts(); return $this->getParts();
} }
/** /**
* Sets the json value, as it would appear in a jCard or jCal object. * Sets the JSON value, as it would appear in a jCard or jCal object.
* *
* The value must always be an array. * The value must always be an array.
*
* @param array $value
* @return void
*/ */
public function setJsonValue(array $value) { public function setJsonValue(array $value)
{
if (count($value)===1) { if (1 === count($value)) {
$this->setValue(reset($value)); $this->setValue(reset($value));
} else { } else {
$this->setValue($value); $this->setValue($value);
} }
} }
/** /**
* This method returns an array, with the representation as it should be * This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents. * encoded in JSON. This is used to create jCard or jCal documents.
* *
* @return array * @return array
*/ */
public function jsonSerialize() { #[\ReturnTypeWillChange]
public function jsonSerialize()
$parameters = array(); {
$parameters = [];
foreach ($this->parameters as $parameter) { foreach ($this->parameters as $parameter) {
if ($parameter->name === 'VALUE') { if ('VALUE' === $parameter->name) {
continue; continue;
} }
$parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize();
@ -311,15 +294,76 @@ abstract class Property extends Node {
} }
return array_merge( return array_merge(
array( [
strtolower($this->name), strtolower($this->name),
(object) $parameters, (object) $parameters,
strtolower($this->getValueType()), strtolower($this->getValueType()),
), ],
$this->getJsonValue() $this->getJsonValue()
); );
} }
/**
* Hydrate data from a XML subtree, as it would appear in a xCard or xCal
* object.
*/
public function setXmlValue(array $value)
{
$this->setJsonValue($value);
}
/**
* This method serializes the data into XML. This is used to create xCard or
* xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
public function xmlSerialize(Xml\Writer $writer)
{
$parameters = [];
foreach ($this->parameters as $parameter) {
if ('VALUE' === $parameter->name) {
continue;
}
$parameters[] = $parameter;
}
$writer->startElement(strtolower($this->name));
if (!empty($parameters)) {
$writer->startElement('parameters');
foreach ($parameters as $parameter) {
$writer->startElement(strtolower($parameter->name));
$writer->write($parameter);
$writer->endElement();
}
$writer->endElement();
}
$this->xmlSerializeValue($writer);
$writer->endElement();
}
/**
* This method serializes only the value of a property. This is used to
* create xCard or xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
protected function xmlSerializeValue(Xml\Writer $writer)
{
$valueType = strtolower($this->getValueType());
foreach ($this->getJsonValue() as $values) {
foreach ((array) $values as $value) {
$writer->writeElement($valueType, $value);
}
}
}
/** /**
* Called when this object is being cast to a string. * Called when this object is being cast to a string.
@ -330,31 +374,36 @@ abstract class Property extends Node {
* *
* @return string * @return string
*/ */
public function __toString() { public function __toString()
{
return (string) $this->getValue(); return (string) $this->getValue();
} }
/* ArrayAccess interface {{{ */ /* ArrayAccess interface {{{ */
/** /**
* Checks if an array element exists * Checks if an array element exists.
* *
* @param mixed $name * @param mixed $name
*
* @return bool * @return bool
*/ */
public function offsetExists($name) { #[\ReturnTypeWillChange]
public function offsetExists($name)
if (is_int($name)) return parent::offsetExists($name); {
if (is_int($name)) {
return parent::offsetExists($name);
}
$name = strtoupper($name); $name = strtoupper($name);
foreach ($this->parameters as $parameter) { foreach ($this->parameters as $parameter) {
if ($parameter->name == $name) return true; if ($parameter->name == $name) {
return true;
}
} }
return false;
return false;
} }
/** /**
@ -363,30 +412,33 @@ abstract class Property extends Node {
* If the parameter does not exist, null is returned. * If the parameter does not exist, null is returned.
* *
* @param string $name * @param string $name
*
* @return Node * @return Node
*/ */
public function offsetGet($name) { #[\ReturnTypeWillChange]
public function offsetGet($name)
if (is_int($name)) return parent::offsetGet($name); {
if (is_int($name)) {
return parent::offsetGet($name);
}
$name = strtoupper($name); $name = strtoupper($name);
if (!isset($this->parameters[$name])) { if (!isset($this->parameters[$name])) {
return null; return;
} }
return $this->parameters[$name]; return $this->parameters[$name];
} }
/** /**
* Creates a new parameter * Creates a new parameter.
* *
* @param string $name * @param string $name
* @param mixed $value * @param mixed $value
* @return void
*/ */
public function offsetSet($name, $value) { #[\ReturnTypeWillChange]
public function offsetSet($name, $value)
{
if (is_int($name)) { if (is_int($name)) {
parent::offsetSet($name, $value); parent::offsetSet($name, $value);
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
@ -398,17 +450,16 @@ abstract class Property extends Node {
$param = new Parameter($this->root, $name, $value); $param = new Parameter($this->root, $name, $value);
$this->parameters[$param->name] = $param; $this->parameters[$param->name] = $param;
} }
/** /**
* Removes one or more parameters with the specified name * Removes one or more parameters with the specified name.
* *
* @param string $name * @param string $name
* @return void
*/ */
public function offsetUnset($name) { #[\ReturnTypeWillChange]
public function offsetUnset($name)
{
if (is_int($name)) { if (is_int($name)) {
parent::offsetUnset($name); parent::offsetUnset($name);
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
@ -419,23 +470,20 @@ abstract class Property extends Node {
} }
unset($this->parameters[strtoupper($name)]); unset($this->parameters[strtoupper($name)]);
} }
/* }}} */ /* }}} */
/** /**
* This method is automatically called when the object is cloned. * This method is automatically called when the object is cloned.
* Specifically, this will ensure all child elements are also cloned. * Specifically, this will ensure all child elements are also cloned.
*
* @return void
*/ */
public function __clone() { public function __clone()
{
foreach ($this->parameters as $key => $child) { foreach ($this->parameters as $key => $child) {
$this->parameters[$key] = clone $child; $this->parameters[$key] = clone $child;
$this->parameters[$key]->parent = $this; $this->parameters[$key]->parent = $this;
} }
} }
/** /**
@ -453,31 +501,45 @@ abstract class Property extends Node {
* * node - (reference to the offending node) * * node - (reference to the offending node)
* *
* @param int $options * @param int $options
*
* @return array * @return array
*/ */
public function validate($options = 0) { public function validate($options = 0)
{
$warnings = array(); $warnings = [];
// Checking if our value is UTF-8 // Checking if our value is UTF-8
if (!StringUtil::isUTF8($this->getRawMimeDirValue())) { if (!StringUtil::isUTF8($this->getRawMimeDirValue())) {
$warnings[] = array( $oldValue = $this->getRawMimeDirValue();
'level' => 1, $level = 3;
'message' => 'Property is not valid UTF-8!',
'node' => $this,
);
if ($options & self::REPAIR) { if ($options & self::REPAIR) {
$this->setRawMimeDirValue(StringUtil::convertToUTF8($this->getRawMimeDirValue())); $newValue = StringUtil::convertToUTF8($oldValue);
if (true || StringUtil::isUTF8($newValue)) {
$this->setRawMimeDirValue($newValue);
$level = 1;
} }
} }
if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) {
$message = 'Property contained a control character (0x'.bin2hex($matches[1]).')';
} else {
$message = 'Property is not valid UTF-8! '.$oldValue;
}
$warnings[] = [
'level' => $level,
'message' => $message,
'node' => $this,
];
}
// Checking if the propertyname does not contain any invalid bytes. // Checking if the propertyname does not contain any invalid bytes.
if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
$warnings[] = array( $warnings[] = [
'level' => 1, 'level' => $options & self::REPAIR ? 1 : 3,
'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', 'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed',
'node' => $this, 'node' => $this,
); ];
if ($options & self::REPAIR) { if ($options & self::REPAIR) {
// Uppercasing and converting underscores to dashes. // Uppercasing and converting underscores to dashes.
$this->name = strtoupper( $this->name = strtoupper(
@ -485,9 +547,52 @@ abstract class Property extends Node {
); );
// Removing every other invalid character // Removing every other invalid character
$this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
}
} }
if ($encoding = $this->offsetGet('ENCODING')) {
if (Document::VCARD40 === $this->root->getDocumentType()) {
$warnings[] = [
'level' => 3,
'message' => 'ENCODING parameter is not valid in vCard 4.',
'node' => $this,
];
} else {
$encoding = (string) $encoding;
$allowedEncoding = [];
switch ($this->root->getDocumentType()) {
case Document::ICALENDAR20:
$allowedEncoding = ['8BIT', 'BASE64'];
break;
case Document::VCARD21:
$allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT'];
break;
case Document::VCARD30:
$allowedEncoding = ['B'];
//Repair vCard30 that use BASE64 encoding
if ($options & self::REPAIR) {
if ('BASE64' === strtoupper($encoding)) {
$encoding = 'B';
$this['ENCODING'] = $encoding;
$warnings[] = [
'level' => 1,
'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.',
'node' => $this,
];
}
}
break;
}
if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) {
$warnings[] = [
'level' => 3,
'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.',
'node' => $this,
];
}
}
} }
// Validating inner parameters // Validating inner parameters
@ -496,7 +601,20 @@ abstract class Property extends Node {
} }
return $warnings; return $warnings;
} }
/**
* Call this method on a document if you're done using it.
*
* It's intended to remove all circular references, so PHP can easily clean
* it up.
*/
public function destroy()
{
parent::destroy();
foreach ($this->parameters as $param) {
$param->destroy();
}
$this->parameters = [];
}
} }

View file

@ -2,12 +2,10 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
use use Sabre\VObject\Property;
LogicException,
Sabre\VObject\Property;
/** /**
* BINARY property * BINARY property.
* *
* This object represents BINARY values. * This object represents BINARY values.
* *
@ -16,19 +14,19 @@ use
* *
* This property will transparently encode and decode to base64. * This property will transparently encode and decode to base64.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Binary extends Property { class Binary extends Property
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = null; public $delimiter = '';
/** /**
* Updates the current value. * Updates the current value.
@ -36,24 +34,18 @@ class Binary extends Property {
* 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 $value
* @return void
*/ */
public function setValue($value) { public function setValue($value)
{
if (is_array($value)) { if (is_array($value)) {
if (1 === count($value)) {
if(count($value) === 1) {
$this->value = $value[0]; $this->value = $value[0];
} else { } else {
throw new \InvalidArgumentException('The argument must either be a string or an array with only one child'); throw new \InvalidArgumentException('The argument must either be a string or an array with only one child');
} }
} else { } else {
$this->value = $value; $this->value = $value;
} }
} }
/** /**
@ -63,12 +55,10 @@ class Binary extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->value = base64_decode($val); $this->value = base64_decode($val);
} }
/** /**
@ -76,10 +66,9 @@ class Binary extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return base64_encode($this->value); return base64_encode($this->value);
} }
/** /**
@ -90,10 +79,9 @@ class Binary extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'BINARY'; return 'BINARY';
} }
/** /**
@ -103,25 +91,19 @@ class Binary extends Property {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
return array(base64_encode($this->getValue())); return [base64_encode($this->getValue())];
} }
/** /**
* Sets the json value, as it would appear in a jCard or jCal object. * Sets the json value, as it would appear in a jCard or jCal object.
* *
* The value must always be an array. * The value must always be an array.
*
* @param array $value
* @return void
*/ */
public function setJsonValue(array $value) { public function setJsonValue(array $value)
{
$value = array_map('base64_decode', $value); $value = array_map('base64_decode', $value);
parent::setJsonValue($value); parent::setJsonValue($value);
} }
} }

View file

@ -2,23 +2,22 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
use use Sabre\VObject\Property;
Sabre\VObject\Property;
/** /**
* Boolean property * Boolean property.
* *
* This object represents BOOLEAN values. These are always the case-insenstive * This object represents BOOLEAN values. These are always the case-insensitive
* string TRUE or FALSE. * string TRUE or FALSE.
* *
* Automatic conversion to PHP's true and false are done. * Automatic conversion to PHP's true and false are done.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Boolean extends Property { class Boolean extends Property
{
/** /**
* Sets a raw value coming from a mimedir (iCalendar/vCard) file. * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
* *
@ -26,13 +25,11 @@ class Boolean extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$val = strtoupper($val)==='TRUE'?true:false; $val = 'TRUE' === strtoupper($val) ? true : false;
$this->setValue($val); $this->setValue($val);
} }
/** /**
@ -40,10 +37,9 @@ class Boolean extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return $this->value ? 'TRUE' : 'FALSE'; return $this->value ? 'TRUE' : 'FALSE';
} }
/** /**
@ -54,10 +50,23 @@ class Boolean extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'BOOLEAN'; return 'BOOLEAN';
} }
/**
* Hydrate data from a XML subtree, as it would appear in a xCard or xCal
* object.
*/
public function setXmlValue(array $value)
{
$value = array_map(
function ($value) {
return 'true' === $value;
},
$value
);
parent::setXmlValue($value);
}
} }

View file

@ -3,7 +3,7 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
/** /**
* FlatText property * FlatText property.
* *
* This object represents certain TEXT values. * This object represents certain TEXT values.
* *
@ -18,14 +18,14 @@ namespace Sabre\VObject\Property;
* vCard 4.0 states something similar. An unescaped semi-colon _may_ be a * vCard 4.0 states something similar. An unescaped semi-colon _may_ be a
* delimiter, depending on the property. * delimiter, depending on the property.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class FlatText extends Text { class FlatText extends Text
{
/** /**
* Field separator * Field separator.
* *
* @var string * @var string
*/ */
@ -37,13 +37,10 @@ class FlatText extends Text {
* Overriding this so we're not splitting on a ; delimiter. * Overriding this so we're not splitting on a ; delimiter.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setQuotedPrintableValue($val) { public function setQuotedPrintableValue($val)
{
$val = quoted_printable_decode($val); $val = quoted_printable_decode($val);
$this->setValue($val); $this->setValue($val);
} }
} }

View file

@ -2,26 +2,26 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
use use Sabre\VObject\Property;
Sabre\VObject\Property; use Sabre\Xml;
/** /**
* Float property * Float property.
* *
* This object represents FLOAT values. These can be 1 or more floating-point * This object represents FLOAT values. These can be 1 or more floating-point
* numbers. * numbers.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class FloatValue extends Property { class FloatValue extends Property
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = ';'; public $delimiter = ';';
@ -32,16 +32,14 @@ class FloatValue extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$val = explode($this->delimiter, $val); $val = explode($this->delimiter, $val);
foreach ($val as &$item) { foreach ($val as &$item) {
$item = (float) $item; $item = (float) $item;
} }
$this->setParts($val); $this->setParts($val);
} }
/** /**
@ -49,13 +47,12 @@ class FloatValue extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return implode( return implode(
$this->delimiter, $this->delimiter,
$this->getParts() $this->getParts()
); );
} }
/** /**
@ -66,36 +63,62 @@ class FloatValue extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "FLOAT"; return 'FLOAT';
} }
/** /**
* Returns the value, in the format it should be encoded for json. * Returns the value, in the format it should be encoded for JSON.
* *
* This method must always return an array. * This method must always return an array.
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
$val = array_map(function($item) { $val = array_map('floatval', $this->getParts());
return (float)$item;
}, $this->getParts());
// Special-casing the GEO property. // Special-casing the GEO property.
// //
// See: // See:
// http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2 // http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2
if ($this->name==='GEO') { if ('GEO' === $this->name) {
return array($val); return [$val];
} else { }
return $val; return $val;
} }
/**
* Hydrate data from a XML subtree, as it would appear in a xCard or xCal
* object.
*/
public function setXmlValue(array $value)
{
$value = array_map('floatval', $value);
parent::setXmlValue($value);
}
/**
* This method serializes only the value of a property. This is used to
* create xCard or xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
protected function xmlSerializeValue(Xml\Writer $writer)
{
// Special-casing the GEO property.
//
// See:
// http://tools.ietf.org/html/rfc6321#section-3.4.1.2
if ('GEO' === $this->name) {
$value = array_map('floatval', $this->getParts());
$writer->writeElement('latitude', $value[0]);
$writer->writeElement('longitude', $value[1]);
} else {
parent::xmlSerializeValue($writer);
}
} }
} }

View file

@ -2,27 +2,26 @@
namespace Sabre\VObject\Property\ICalendar; namespace Sabre\VObject\Property\ICalendar;
use use Sabre\VObject\Property\Text;
Sabre\VObject\Property\Text;
/** /**
* CalAddress property * CalAddress property.
* *
* This object encodes CAL-ADDRESS values, as defined in rfc5545 * This object encodes CAL-ADDRESS values, as defined in rfc5545
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class CalAddress extends Text { class CalAddress extends Text
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = null; public $delimiter = '';
/** /**
* Returns the type of value. * Returns the type of value.
@ -32,10 +31,29 @@ class CalAddress extends Text {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'CAL-ADDRESS'; return 'CAL-ADDRESS';
} }
/**
* This returns a normalized form of the value.
*
* This is primarily used right now to turn mixed-cased schemes in user
* uris to lower-case.
*
* Evolution in particular tends to encode mailto: as MAILTO:.
*
* @return string
*/
public function getNormalizedValue()
{
$input = $this->getValue();
if (!strpos($input, ':')) {
return $input;
}
list($schema, $everythingElse) = explode(':', $input, 2);
return strtolower($schema).':'.$everythingElse;
}
} }

View file

@ -2,23 +2,17 @@
namespace Sabre\VObject\Property\ICalendar; namespace Sabre\VObject\Property\ICalendar;
use
Sabre\VObject\Property,
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\DateTimeParser,
Sabre\VObject\TimeZoneUtil;
/** /**
* DateTime property * DateTime property.
* *
* This object represents DATE values, as defined here: * This object represents DATE values, as defined here:
* *
* http://tools.ietf.org/html/rfc5545#section-3.3.5 * http://tools.ietf.org/html/rfc5545#section-3.3.5
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Date extends DateTime { class Date extends DateTime
{
} }

View file

@ -2,14 +2,15 @@
namespace Sabre\VObject\Property\ICalendar; namespace Sabre\VObject\Property\ICalendar;
use use DateTimeInterface;
Sabre\VObject\Property, use DateTimeZone;
Sabre\VObject\Parser\MimeDir, use Sabre\VObject\DateTimeParser;
Sabre\VObject\DateTimeParser, use Sabre\VObject\InvalidDataException;
Sabre\VObject\TimeZoneUtil; use Sabre\VObject\Property;
use Sabre\VObject\TimeZoneUtil;
/** /**
* DateTime property * DateTime property.
* *
* This object represents DATE-TIME values, as defined here: * This object represents DATE-TIME values, as defined here:
* *
@ -19,12 +20,12 @@ use
* cases represent a DATE value. This is because it's a common usecase to be * cases represent a DATE value. This is because it's a common usecase to be
* able to change a DATE-TIME into a DATE. * able to change a DATE-TIME into a DATE.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class DateTime extends Property { class DateTime extends Property
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
@ -37,18 +38,14 @@ class DateTime extends Property {
* Sets a multi-valued property. * Sets a multi-valued property.
* *
* You may also specify DateTime objects here. * You may also specify DateTime objects here.
*
* @param array $parts
* @return void
*/ */
public function setParts(array $parts) { public function setParts(array $parts)
{
if (isset($parts[0]) && $parts[0] instanceof \DateTime) { if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) {
$this->setDateTimes($parts); $this->setDateTimes($parts);
} else { } else {
parent::setParts($parts); parent::setParts($parts);
} }
} }
/** /**
@ -58,19 +55,17 @@ class DateTime extends Property {
* *
* Instead of strings, you may also use DateTime here. * Instead of strings, you may also use DateTime here.
* *
* @param string|array|\DateTime $value * @param string|array|DateTimeInterface $value
* @return void
*/ */
public function setValue($value) { public function setValue($value)
{
if (is_array($value) && isset($value[0]) && $value[0] instanceof \DateTime) { if (is_array($value) && isset($value[0]) && $value[0] instanceof DateTimeInterface) {
$this->setDateTimes($value); $this->setDateTimes($value);
} elseif ($value instanceof \DateTime) { } elseif ($value instanceof DateTimeInterface) {
$this->setDateTimes(array($value)); $this->setDateTimes([$value]);
} else { } else {
parent::setValue($value); parent::setValue($value);
} }
} }
/** /**
@ -80,12 +75,10 @@ class DateTime extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->setValue(explode($this->delimiter, $val)); $this->setValue(explode($this->delimiter, $val));
} }
/** /**
@ -93,10 +86,9 @@ class DateTime extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return implode($this->delimiter, $this->getParts()); return implode($this->delimiter, $this->getParts());
} }
/** /**
@ -104,10 +96,24 @@ class DateTime extends Property {
* *
* @return bool * @return bool
*/ */
public function hasTime() { public function hasTime()
{
return strtoupper((string)$this['VALUE']) !== 'DATE'; return 'DATE' !== strtoupper((string) $this['VALUE']);
}
/**
* Returns true if this is a floating DATE or DATE-TIME.
*
* Note that DATE is always floating.
*/
public function isFloating()
{
return
!$this->hasTime() ||
(
!isset($this['TZID']) &&
false === strpos($this->getValue(), 'Z')
);
} }
/** /**
@ -117,85 +123,93 @@ class DateTime extends Property {
* first will be returned. To get an array with multiple values, call * first will be returned. To get an array with multiple values, call
* getDateTimes. * getDateTimes.
* *
* @return \DateTime * If no timezone information is known, because it's either an all-day
* property or floating time, we will use the DateTimeZone argument to
* figure out the exact date.
*
* @param DateTimeZone $timeZone
*
* @return \DateTimeImmutable
*/ */
public function getDateTime() { public function getDateTime(DateTimeZone $timeZone = null)
{
$dt = $this->getDateTimes(); $dt = $this->getDateTimes($timeZone);
if (!$dt) return null; if (!$dt) {
return;
}
return $dt[0]; return $dt[0];
} }
/** /**
* Returns multiple date-time values. * Returns multiple date-time values.
* *
* If no timezone information is known, because it's either an all-day
* property or floating time, we will use the DateTimeZone argument to
* figure out the exact date.
*
* @param DateTimeZone $timeZone
*
* @return \DateTimeImmutable[]
* @return \DateTime[] * @return \DateTime[]
*/ */
public function getDateTimes() { public function getDateTimes(DateTimeZone $timeZone = null)
{
// Does the property have a TZID?
$tzid = $this['TZID'];
// Finding the timezone. if ($tzid) {
$tz = $this['TZID']; $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root);
if ($tz) {
$tz = TimeZoneUtil::getTimeZone((string)$tz, $this->root);
} }
$dts = array(); $dts = [];
foreach ($this->getParts() as $part) { foreach ($this->getParts() as $part) {
$dts[] = DateTimeParser::parse($part, $tz); $dts[] = DateTimeParser::parse($part, $timeZone);
} }
return $dts;
return $dts;
} }
/** /**
* Sets the property as a DateTime object. * Sets the property as a DateTime object.
* *
* @param \DateTime $dt * @param bool isFloating If set to true, timezones will be ignored
* @param bool isFloating If set to true, timezones will be ignored.
* @return void
*/ */
public function setDateTime(\DateTime $dt, $isFloating = false) { public function setDateTime(DateTimeInterface $dt, $isFloating = false)
{
$this->setDateTimes(array($dt), $isFloating); $this->setDateTimes([$dt], $isFloating);
} }
/** /**
* Sets the property as multiple date-time objects. * Sets the property as multiple date-time objects.
* *
* The first value will be used as a reference for the timezones, and all * The first value will be used as a reference for the timezones, and all
* the otehr values will be adjusted for that timezone * the other values will be adjusted for that timezone
* *
* @param \DateTime[] $dt * @param DateTimeInterface[] $dt
* @param bool isFloating If set to true, timezones will be ignored. * @param bool isFloating If set to true, timezones will be ignored
* @return void
*/ */
public function setDateTimes(array $dt, $isFloating = false) { public function setDateTimes(array $dt, $isFloating = false)
{
$values = array(); $values = [];
if ($this->hasTime()) { if ($this->hasTime()) {
$tz = null; $tz = null;
$isUtc = false; $isUtc = false;
foreach ($dt as $d) { foreach ($dt as $d) {
if ($isFloating) { if ($isFloating) {
$values[] = $d->format('Ymd\\THis'); $values[] = $d->format('Ymd\\THis');
continue; continue;
} }
if (is_null($tz)) { if (is_null($tz)) {
$tz = $d->getTimeZone(); $tz = $d->getTimeZone();
$isUtc = in_array($tz->getName() , array('UTC', 'GMT', 'Z')); $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']);
if (!$isUtc) { if (!$isUtc) {
$this->offsetSet('TZID', $tz->getName()); $this->offsetSet('TZID', $tz->getName());
} }
} else { } else {
$d->setTimeZone($tz); $d = $d->setTimeZone($tz);
} }
if ($isUtc) { if ($isUtc) {
@ -203,25 +217,18 @@ class DateTime extends Property {
} else { } else {
$values[] = $d->format('Ymd\\THis'); $values[] = $d->format('Ymd\\THis');
} }
} }
if ($isUtc || $isFloating) { if ($isUtc || $isFloating) {
$this->offsetUnset('TZID'); $this->offsetUnset('TZID');
} }
} else { } else {
foreach ($dt as $d) { foreach ($dt as $d) {
$values[] = $d->format('Ymd'); $values[] = $d->format('Ymd');
} }
$this->offsetUnset('TZID'); $this->offsetUnset('TZID');
} }
$this->value = $values; $this->value = $values;
} }
/** /**
@ -232,77 +239,126 @@ class DateTime extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return $this->hasTime() ? 'DATE-TIME' : 'DATE'; return $this->hasTime() ? 'DATE-TIME' : 'DATE';
} }
/** /**
* Returns the value, in the format it should be encoded for json. * Returns the value, in the format it should be encoded for JSON.
* *
* This method must always return an array. * This method must always return an array.
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
$dts = $this->getDateTimes(); $dts = $this->getDateTimes();
$hasTime = $this->hasTime(); $hasTime = $this->hasTime();
$isFloating = $this->isFloating();
$tz = $dts[0]->getTimeZone(); $tz = $dts[0]->getTimeZone();
$isUtc = in_array($tz->getName() , array('UTC', 'GMT', 'Z')); $isUtc = $isFloating ? false : in_array($tz->getName(), ['UTC', 'GMT', 'Z']);
return array_map(function($dt) use ($hasTime, $isUtc) {
return array_map(
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 { } else {
return $dt->format('Y-m-d'); return $dt->format('Y-m-d');
} }
},
}, $dts); $dts
);
} }
/** /**
* Sets the json value, as it would appear in a jCard or jCal object. * Sets the json value, as it would appear in a jCard or jCal object.
* *
* The value must always be an array. * The value must always be an array.
*
* @param array $value
* @return void
*/ */
public function setJsonValue(array $value) { public function setJsonValue(array $value)
{
// dates and times in jCal have one difference to dates and times in // dates and times in jCal have one difference to dates and times in
// iCalendar. In jCal date-parts are separated by dashes, and // iCalendar. In jCal date-parts are separated by dashes, and
// time-parts are separated by colons. It makes sense to just remove // time-parts are separated by colons. It makes sense to just remove
// those. // those.
$this->setValue(array_map(function($item) { $this->setValue(
array_map(
return strtr($item, array(':'=>'', '-'=>'')); function ($item) {
return strtr($item, [':' => '', '-' => '']);
}, $value)); },
$value
)
);
} }
/** /**
* We need to intercept offsetSet, because it may be used to alter the * We need to intercept offsetSet, because it may be used to alter the
* VALUE from DATE-TIME to DATE or vice-versa. * VALUE from DATE-TIME to DATE or vice-versa.
* *
* @param string $name * @param string $name
* @param mixed $value * @param mixed $value
* @return void
*/ */
public function offsetSet($name, $value) { #[\ReturnTypeWillChange]
public function offsetSet($name, $value)
{
parent::offsetSet($name, $value); parent::offsetSet($name, $value);
if (strtoupper($name)!=='VALUE') { if ('VALUE' !== strtoupper($name)) {
return; return;
} }
// This will ensure that dates are correctly encoded. // This will ensure that dates are correctly encoded.
$this->setDateTimes($this->getDateTimes()); $this->setDateTimes($this->getDateTimes());
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* Node::REPAIR - May attempt to automatically repair the problem.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on)
* 2 - An inconsequential issue
* 3 - A severe issue.
*
* @param int $options
*
* @return array
*/
public function validate($options = 0)
{
$messages = parent::validate($options);
$valueType = $this->getValueType();
$values = $this->getParts();
foreach ($values as $value) {
try {
switch ($valueType) {
case 'DATE':
DateTimeParser::parseDate($value);
break;
case 'DATE-TIME':
DateTimeParser::parseDateTime($value);
break;
}
} catch (InvalidDataException $e) {
$messages[] = [
'level' => 3,
'message' => 'The supplied value ('.$value.') is not a correct '.$valueType,
'node' => $this,
];
break;
}
}
return $messages;
} }
} }

View file

@ -2,29 +2,27 @@
namespace Sabre\VObject\Property\ICalendar; namespace Sabre\VObject\Property\ICalendar;
use use Sabre\VObject\DateTimeParser;
Sabre\VObject\Property, use Sabre\VObject\Property;
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\DateTimeParser;
/** /**
* Duration property * Duration property.
* *
* This object represents DURATION values, as defined here: * This object represents DURATION values, as defined here:
* *
* http://tools.ietf.org/html/rfc5545#section-3.3.6 * http://tools.ietf.org/html/rfc5545#section-3.3.6
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Duration extends Property { class Duration extends Property
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = ','; public $delimiter = ',';
@ -35,12 +33,10 @@ class Duration extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->setValue(explode($this->delimiter, $val)); $this->setValue(explode($this->delimiter, $val));
} }
/** /**
@ -48,10 +44,9 @@ class Duration extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return implode($this->delimiter, $this->getParts()); return implode($this->delimiter, $this->getParts());
} }
/** /**
@ -62,10 +57,9 @@ class Duration extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'DURATION'; return 'DURATION';
} }
/** /**
@ -75,12 +69,11 @@ class Duration extends Property {
* *
* @return \DateInterval * @return \DateInterval
*/ */
public function getDateInterval() { public function getDateInterval()
{
$parts = $this->getParts(); $parts = $this->getParts();
$value = $parts[0]; $value = $parts[0];
return DateTimeParser::parseDuration($value); return DateTimeParser::parseDuration($value);
} }
} }

View file

@ -2,29 +2,28 @@
namespace Sabre\VObject\Property\ICalendar; namespace Sabre\VObject\Property\ICalendar;
use use Sabre\VObject\DateTimeParser;
Sabre\VObject\Property, use Sabre\VObject\Property;
Sabre\VObject\Parser\MimeDir, use Sabre\Xml;
Sabre\VObject\DateTimeParser;
/** /**
* Period property * Period property.
* *
* This object represents PERIOD values, as defined here: * This object represents PERIOD values, as defined here:
* *
* http://tools.ietf.org/html/rfc5545#section-3.8.2.6 * http://tools.ietf.org/html/rfc5545#section-3.8.2.6
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Period extends Property { class Period extends Property
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = ','; public $delimiter = ',';
@ -35,12 +34,10 @@ class Period extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->setValue(explode($this->delimiter, $val)); $this->setValue(explode($this->delimiter, $val));
} }
/** /**
@ -48,10 +45,9 @@ class Period extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return implode($this->delimiter, $this->getParts()); return implode($this->delimiter, $this->getParts());
} }
/** /**
@ -62,29 +58,25 @@ class Period extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "PERIOD"; return 'PERIOD';
} }
/** /**
* Sets the json value, as it would appear in a jCard or jCal object. * Sets the json value, as it would appear in a jCard or jCal object.
* *
* The value must always be an array. * The value must always be an array.
*
* @param array $value
* @return void
*/ */
public function setJsonValue(array $value) { public function setJsonValue(array $value)
{
$value = array_map(function($item) { $value = array_map(
function ($item) {
return strtr(implode('/', $item), array(':' => '', '-' => '')); return strtr(implode('/', $item), [':' => '', '-' => '']);
},
}, $value); $value
);
parent::setJsonValue($value); parent::setJsonValue($value);
} }
/** /**
@ -94,33 +86,50 @@ class Period extends Property {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
$return = array(); $return = [];
foreach ($this->getParts() as $item) { foreach ($this->getParts() as $item) {
list($start, $end) = explode('/', $item, 2); list($start, $end) = explode('/', $item, 2);
$start = DateTimeParser::parseDateTime($start); $start = DateTimeParser::parseDateTime($start);
// This is a duration value. // This is a duration value.
if ($end[0]==='P') { if ('P' === $end[0]) {
$return[] = array( $return[] = [
$start->format('Y-m-d\\TH:i:s'), $start->format('Y-m-d\\TH:i:s'),
$end $end,
); ];
} else { } else {
$end = DateTimeParser::parseDateTime($end); $end = DateTimeParser::parseDateTime($end);
$return[] = array( $return[] = [
$start->format('Y-m-d\\TH:i:s'), $start->format('Y-m-d\\TH:i:s'),
$end->format('Y-m-d\\TH:i:s'), $end->format('Y-m-d\\TH:i:s'),
); ];
} }
} }
return $return; return $return;
} }
/**
* This method serializes only the value of a property. This is used to
* create xCard or xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
protected function xmlSerializeValue(Xml\Writer $writer)
{
$writer->startElement(strtolower($this->getValueType()));
$value = $this->getJsonValue();
$writer->writeElement('start', $value[0][0]);
if ('P' === $value[0][1][0]) {
$writer->writeElement('duration', $value[0][1]);
} else {
$writer->writeElement('end', $value[0][1]);
}
$writer->endElement();
}
} }

View file

@ -2,12 +2,11 @@
namespace Sabre\VObject\Property\ICalendar; namespace Sabre\VObject\Property\ICalendar;
use use Sabre\VObject\Property;
Sabre\VObject\Property, use Sabre\Xml;
Sabre\VObject\Parser\MimeDir;
/** /**
* Recur property * Recur property.
* *
* This object represents RECUR properties. * This object represents RECUR properties.
* These values are just used for RRULE and the now deprecated EXRULE. * These values are just used for RRULE and the now deprecated EXRULE.
@ -19,39 +18,40 @@ use
* This property exposes this as a key=>value array that is accessible using * This property exposes this as a key=>value array that is accessible using
* getParts, and may be set using setParts. * getParts, and may be set using setParts.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Recur extends Property { class Recur extends Property
{
/** /**
* 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 $value
* @return void
*/ */
public function setValue($value) { public function setValue($value)
{
// If we're getting the data from json, we'll be receiving an object // If we're getting the data from json, we'll be receiving an object
if ($value instanceof \StdClass) { if ($value instanceof \StdClass) {
$value = (array) $value; $value = (array) $value;
} }
if (is_array($value)) { if (is_array($value)) {
$newVal = array(); $newVal = [];
foreach ($value as $k => $v) { foreach ($value as $k => $v) {
if (is_string($v)) { if (is_string($v)) {
$v = strtoupper($v); $v = strtoupper($v);
// The value had multiple sub-values // The value had multiple sub-values
if (strpos($v,',')!==false) { if (false !== strpos($v, ',')) {
$v = explode(',', $v); $v = explode(',', $v);
} }
} else { if (0 === strcmp($k, 'until')) {
$v = strtr($v, [':' => '', '-' => '']);
}
} elseif (is_array($v)) {
$v = array_map('strtoupper', $v); $v = array_map('strtoupper', $v);
} }
@ -59,28 +59,10 @@ class Recur extends Property {
} }
$this->value = $newVal; $this->value = $newVal;
} elseif (is_string($value)) { } elseif (is_string($value)) {
$value = strtoupper($value); $this->value = self::stringToArray($value);
$newValue = array();
foreach(explode(';', $value) as $part) {
// Skipping empty parts.
if (empty($part)) {
continue;
}
list($partName, $partValue) = explode('=', $part);
// The value itself had multiple values..
if (strpos($partValue,',')!==false) {
$partValue=explode(',', $partValue);
}
$newValue[$partName] = $partValue;
}
$this->value = $newValue;
} else { } else {
throw new \InvalidArgumentException('You must either pass a string, or a key=>value array'); throw new \InvalidArgumentException('You must either pass a string, or a key=>value array');
} }
} }
/** /**
@ -94,26 +76,22 @@ class Recur extends Property {
* *
* @return string * @return string
*/ */
public function getValue() { public function getValue()
{
$out = array(); $out = [];
foreach ($this->value as $key => $value) { foreach ($this->value as $key => $value) {
$out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value); $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value);
} }
return strtoupper(implode(';',$out));
return strtoupper(implode(';', $out));
} }
/** /**
* Sets a multi-valued property. * Sets a multi-valued property.
*
* @param array $parts
* @return void
*/ */
public function setParts(array $parts) { public function setParts(array $parts)
{
$this->setValue($parts); $this->setValue($parts);
} }
/** /**
@ -124,10 +102,9 @@ class Recur extends Property {
* *
* @return array * @return array
*/ */
public function getParts() { public function getParts()
{
return $this->value; return $this->value;
} }
/** /**
@ -137,12 +114,10 @@ class Recur extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->setValue($val); $this->setValue($val);
} }
/** /**
@ -150,10 +125,9 @@ class Recur extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return $this->getValue(); return $this->getValue();
} }
/** /**
@ -164,10 +138,9 @@ class Recur extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "RECUR"; return 'RECUR';
} }
/** /**
@ -177,13 +150,187 @@ class Recur extends Property {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
$values = array(); $values = [];
foreach ($this->getParts() as $k => $v) { foreach ($this->getParts() as $k => $v) {
if (0 === strcmp($k, 'UNTIL')) {
$date = new DateTime($this->root, null, $v);
$values[strtolower($k)] = $date->getJsonValue()[0];
} elseif (0 === strcmp($k, 'COUNT')) {
$values[strtolower($k)] = intval($v);
} else {
$values[strtolower($k)] = $v; $values[strtolower($k)] = $v;
} }
return array($values); }
return [$values];
}
/**
* This method serializes only the value of a property. This is used to
* create xCard or xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
protected function xmlSerializeValue(Xml\Writer $writer)
{
$valueType = strtolower($this->getValueType());
foreach ($this->getJsonValue() as $value) {
$writer->writeElement($valueType, $value);
}
}
/**
* Parses an RRULE value string, and turns it into a struct-ish array.
*
* @param string $value
*
* @return array
*/
public static function stringToArray($value)
{
$value = strtoupper($value);
$newValue = [];
foreach (explode(';', $value) as $part) {
// Skipping empty parts.
if (empty($part)) {
continue;
}
list($partName, $partValue) = explode('=', $part);
// The value itself had multiple values..
if (false !== strpos($partValue, ',')) {
$partValue = explode(',', $partValue);
}
$newValue[$partName] = $partValue;
}
return $newValue;
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* Node::REPAIR - May attempt to automatically repair the problem.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on)
* 2 - An inconsequential issue
* 3 - A severe issue.
*
* @param int $options
*
* @return array
*/
public function validate($options = 0)
{
$repair = ($options & self::REPAIR);
$warnings = parent::validate($options);
$values = $this->getParts();
foreach ($values as $key => $value) {
if ('' === $value) {
$warnings[] = [
'level' => $repair ? 1 : 3,
'message' => 'Invalid value for '.$key.' in '.$this->name,
'node' => $this,
];
if ($repair) {
unset($values[$key]);
}
} elseif ('BYMONTH' == $key) {
$byMonth = (array) $value;
foreach ($byMonth as $i => $v) {
if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) {
$warnings[] = [
'level' => $repair ? 1 : 3,
'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!',
'node' => $this,
];
if ($repair) {
if (is_array($value)) {
unset($values[$key][$i]);
} else {
unset($values[$key]);
}
}
}
}
// if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) {
unset($values[$key]);
}
} elseif ('BYWEEKNO' == $key) {
$byWeekNo = (array) $value;
foreach ($byWeekNo as $i => $v) {
if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) {
$warnings[] = [
'level' => $repair ? 1 : 3,
'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!',
'node' => $this,
];
if ($repair) {
if (is_array($value)) {
unset($values[$key][$i]);
} else {
unset($values[$key]);
}
}
}
}
// if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) {
unset($values[$key]);
}
} elseif ('BYYEARDAY' == $key) {
$byYearDay = (array) $value;
foreach ($byYearDay as $i => $v) {
if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) {
$warnings[] = [
'level' => $repair ? 1 : 3,
'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!',
'node' => $this,
];
if ($repair) {
if (is_array($value)) {
unset($values[$key][$i]);
} else {
unset($values[$key]);
}
}
}
}
// if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) {
unset($values[$key]);
}
}
}
if (!isset($values['FREQ'])) {
$warnings[] = [
'level' => $repair ? 1 : 3,
'message' => 'FREQ is required in '.$this->name,
'node' => $this,
];
if ($repair) {
$this->parent->remove($this);
}
}
if ($repair) {
$this->setValue($values);
}
return $warnings;
} }
} }

View file

@ -2,21 +2,20 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
use use Sabre\VObject\Property;
Sabre\VObject\Property;
/** /**
* Integer property * Integer property.
* *
* This object represents INTEGER values. These are always a single integer. * This object represents INTEGER values. These are always a single integer.
* They may be preceeded by either + or -. * They may be preceded by either + or -.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Integer extends Property { class IntegerValue extends Property
{
/** /**
* Sets a raw value coming from a mimedir (iCalendar/vCard) file. * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
* *
@ -24,12 +23,10 @@ class Integer extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->setValue((int) $val); $this->setValue((int) $val);
} }
/** /**
@ -37,10 +34,9 @@ class Integer extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return $this->value; return $this->value;
} }
/** /**
@ -51,10 +47,9 @@ class Integer extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "INTEGER"; return 'INTEGER';
} }
/** /**
@ -64,9 +59,18 @@ class Integer extends Property {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
return array((int)$this->getValue()); return [(int) $this->getValue()];
}
/**
* Hydrate data from a XML subtree, as it would appear in a xCard or xCal
* object.
*/
public function setXmlValue(array $value)
{
$value = array_map('intval', $value);
parent::setXmlValue($value);
} }
} }

View file

@ -2,23 +2,23 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
use use Sabre\VObject\Component;
Sabre\VObject\Property, use Sabre\VObject\Document;
Sabre\VObject\Component, use Sabre\VObject\Parser\MimeDir;
Sabre\VObject\Parser\MimeDir, use Sabre\VObject\Property;
Sabre\VObject\Document; use Sabre\Xml;
/** /**
* Text property * Text property.
* *
* This object represents TEXT values. * This object represents TEXT values.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Text extends Property { class Text extends Property
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
@ -32,16 +32,17 @@ class Text extends Property {
* *
* @var array * @var array
*/ */
protected $structuredValues = array( protected $structuredValues = [
// vCard // vCard
'N', 'N',
'ADR', 'ADR',
'ORG', 'ORG',
'GENDER', 'GENDER',
'CLIENTPIDMAP',
// iCalendar // iCalendar
'REQUEST-STATUS', 'REQUEST-STATUS',
); ];
/** /**
* Some text components have a minimum number of components. * Some text components have a minimum number of components.
@ -51,10 +52,10 @@ class Text extends Property {
* *
* @var array * @var array
*/ */
protected $minimumPropertyValues = array( protected $minimumPropertyValues = [
'N' => 5, 'N' => 5,
'ADR' => 7, 'ADR' => 7,
); ];
/** /**
* Creates the property. * Creates the property.
@ -68,10 +69,9 @@ class Text extends Property {
* @param string|array|null $value * @param string|array|null $value
* @param array $parameters List of parameters * @param array $parameters List of parameters
* @param string $group The vcard property group * @param string $group The vcard property group
* @return void
*/ */
public function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) { public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null)
{
// There's two types of multi-valued text properties: // There's two types of multi-valued text properties:
// 1. multivalue properties. // 1. multivalue properties.
// 2. structured value properties // 2. structured value properties
@ -82,10 +82,8 @@ class Text extends Property {
} }
parent::__construct($root, $name, $value, $parameters, $group); parent::__construct($root, $name, $value, $parameters, $group);
} }
/** /**
* Sets a raw value coming from a mimedir (iCalendar/vCard) file. * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
* *
@ -93,22 +91,19 @@ class Text extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); $this->setValue(MimeDir::unescapeValue($val, $this->delimiter));
} }
/** /**
* Sets the value as a quoted-printable encoded string. * Sets the value as a quoted-printable encoded string.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setQuotedPrintableValue($val) { public function setQuotedPrintableValue($val)
{
$val = quoted_printable_decode($val); $val = quoted_printable_decode($val);
// Quoted printable only appears in vCard 2.1, and the only character // Quoted printable only appears in vCard 2.1, and the only character
@ -116,11 +111,10 @@ class Text extends Property {
// that. // that.
// //
// We also don't have to unescape \\, so all we need to look for is a ; // We also don't have to unescape \\, so all we need to look for is a ;
// that's not preceeded with a \. // that's not preceded with a \.
$regex = '# (?<!\\\\) ; #x'; $regex = '# (?<!\\\\) ; #x';
$matches = preg_split($regex, $val); $matches = preg_split($regex, $val);
$this->setValue($matches); $this->setValue($matches);
} }
/** /**
@ -128,8 +122,8 @@ class Text extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
$val = $this->getParts(); $val = $this->getParts();
if (isset($this->minimumPropertyValues[$this->name])) { if (isset($this->minimumPropertyValues[$this->name])) {
@ -137,26 +131,26 @@ class Text extends Property {
} }
foreach ($val as &$item) { foreach ($val as &$item) {
if (!is_array($item)) { if (!is_array($item)) {
$item = array($item); $item = [$item];
} }
foreach ($item as &$subItem) { foreach ($item as &$subItem) {
$subItem = strtr($subItem, array( $subItem = strtr(
$subItem,
[
'\\' => '\\\\', '\\' => '\\\\',
';' => '\;', ';' => '\;',
',' => '\,', ',' => '\,',
"\n" => '\n', "\n" => '\n',
"\r" => "", "\r" => '',
)); ]
);
} }
$item = implode(',', $item); $item = implode(',', $item);
} }
return implode($this->delimiter, $val); return implode($this->delimiter, $val);
} }
/** /**
@ -166,17 +160,16 @@ class Text extends Property {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
// 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)) {
return array($this->getParts()); return [$this->getParts()];
} else {
return $this->getParts();
} }
return $this->getParts();
} }
/** /**
@ -187,10 +180,9 @@ class Text extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "TEXT"; return 'TEXT';
} }
/** /**
@ -198,47 +190,44 @@ class Text extends Property {
* *
* @return string * @return string
*/ */
public function serialize() { public function serialize()
{
// We need to kick in a special type of encoding, if it's a 2.1 vcard. // We need to kick in a special type of encoding, if it's a 2.1 vcard.
if ($this->root->getDocumentType() !== Document::VCARD21) { if (Document::VCARD21 !== $this->root->getDocumentType()) {
return parent::serialize(); return parent::serialize();
} }
$val = $this->getParts(); $val = $this->getParts();
if (isset($this->minimumPropertyValues[$this->name])) { if (isset($this->minimumPropertyValues[$this->name])) {
$val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); $val = \array_pad($val, $this->minimumPropertyValues[$this->name], '');
} }
// Imploding multiple parts into a single value, and splitting the // Imploding multiple parts into a single value, and splitting the
// values with ;. // values with ;.
if (count($val)>1) { if (\count($val) > 1) {
foreach ($val as $k => $v) { foreach ($val as $k => $v) {
$val[$k] = str_replace(';','\;', $v); $val[$k] = \str_replace(';', '\;', $v);
} }
$val = implode(';', $val); $val = \implode(';', $val);
} else { } else {
$val = $val[0]; $val = $val[0];
} }
$str = $this->name; $str = $this->name;
if ($this->group) $str = $this->group . '.' . $this->name; if ($this->group) {
$str = $this->group.'.'.$this->name;
}
foreach ($this->parameters as $param) { foreach ($this->parameters as $param) {
if ('QUOTED-PRINTABLE' === $param->getValue()) {
if ($param->getValue() === 'QUOTED-PRINTABLE') {
continue; continue;
} }
$str .= ';'.$param->serialize(); $str .= ';'.$param->serialize();
} }
// 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 (strpos($val,"\n") !== false) { if (false !== \strpos($val, "\n")) {
$str .= ';ENCODING=QUOTED-PRINTABLE:'; $str .= ';ENCODING=QUOTED-PRINTABLE:';
$lastLine = $str; $lastLine = $str;
$out = null; $out = null;
@ -247,45 +236,117 @@ 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($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($val[$ii]));
} }
if (strlen($lastLine)>=75) { if (\strlen($lastLine) >= 75) {
// Soft line break // Soft line break
$out .= $lastLine."=\r\n "; $out .= $lastLine."=\r\n ";
$lastLine = null; $lastLine = null;
} }
} }
if (!is_null($lastLine)) $out.= $lastLine . "\r\n"; if (!\is_null($lastLine)) {
return $out; $out .= $lastLine."\r\n";
}
return $out;
} else { } else {
$str .= ':'.$val; $str .= ':'.$val;
$out = '';
while(strlen($str)>0) { $str = \preg_replace(
if (strlen($str)>75) { '/(
$out.= mb_strcut($str,0,75,'utf-8') . "\r\n"; (?:^.)? # 1 additional byte in first line because of missing single space (see next line)
$str = ' ' . mb_strcut($str,75,strlen($str),'utf-8'); .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
} else { (?![\x80-\xbf]) # prevent splitting multibyte characters
$out.=$str . "\r\n"; )/x',
$str=''; "$1\r\n ",
$str
);
// remove single space after last CRLF
return \substr($str, 0, -1);
}
}
/**
* This method serializes only the value of a property. This is used to
* create xCard or xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
protected function xmlSerializeValue(Xml\Writer $writer)
{
$values = $this->getParts();
$map = function ($items) use ($values, $writer) {
foreach ($items as $i => $item) {
$writer->writeElement(
$item,
!empty($values[$i]) ? $values[$i] : null
);
}
};
switch ($this->name) {
// Special-casing the REQUEST-STATUS property.
//
// See:
// http://tools.ietf.org/html/rfc6321#section-3.4.1.3
case 'REQUEST-STATUS':
$writer->writeElement('code', $values[0]);
$writer->writeElement('description', $values[1]);
if (isset($values[2])) {
$writer->writeElement('data', $values[2]);
}
break; break;
case 'N':
$map([
'surname',
'given',
'additional',
'prefix',
'suffix',
]);
break;
case 'GENDER':
$map([
'sex',
'text',
]);
break;
case 'ADR':
$map([
'pobox',
'ext',
'street',
'locality',
'region',
'code',
'country',
]);
break;
case 'CLIENTPIDMAP':
$map([
'sourceid',
'uri',
]);
break;
default:
parent::xmlSerializeValue($writer);
} }
} }
return $out;
}
}
/** /**
* Validates the node for correctness. * Validates the node for correctness.
* *
@ -301,30 +362,29 @@ class Text extends Property {
* * node - (reference to the offending node) * * node - (reference to the offending node)
* *
* @param int $options * @param int $options
*
* @return array * @return array
*/ */
public function validate($options = 0) { public function validate($options = 0)
{
$warnings = parent::validate($options); $warnings = parent::validate($options);
if (isset($this->minimumPropertyValues[$this->name])) { if (isset($this->minimumPropertyValues[$this->name])) {
$minimum = $this->minimumPropertyValues[$this->name]; $minimum = $this->minimumPropertyValues[$this->name];
$parts = $this->getParts(); $parts = $this->getParts();
if (count($parts) < $minimum) { if (count($parts) < $minimum) {
$warnings[] = array( $warnings[] = [
'level' => 1, 'level' => $options & self::REPAIR ? 1 : 3,
'message' => 'This property must have at least ' . $minimum . ' components. It only has ' . count($parts), 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.count($parts),
'node' => $this, 'node' => $this,
); ];
if ($options & self::REPAIR) { if ($options & self::REPAIR) {
$parts = array_pad($parts, $minimum, ''); $parts = array_pad($parts, $minimum, '');
$this->setParts($parts); $this->setParts($parts);
} }
} }
} }
return $warnings; return $warnings;
} }
} }

View file

@ -5,23 +5,23 @@ namespace Sabre\VObject\Property;
use Sabre\VObject\DateTimeParser; use Sabre\VObject\DateTimeParser;
/** /**
* Time property * Time property.
* *
* This object encodes TIME values. * This object encodes TIME values.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Time extends Text { class Time extends Text
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = null; public $delimiter = '';
/** /**
* Returns the type of value. * Returns the type of value.
@ -31,10 +31,30 @@ class Time extends Text {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'TIME';
}
return "TIME"; /**
* Sets the JSON value, as it would appear in a jCard or jCal object.
*
* The value must always be an array.
*/
public function setJsonValue(array $value)
{
// Removing colons from value.
$value = str_replace(
':',
'',
$value
);
if (1 === count($value)) {
$this->setValue(reset($value));
} else {
$this->setValue($value);
}
} }
/** /**
@ -44,10 +64,9 @@ class Time extends Text {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
$parts = DateTimeParser::parseVCardTime($this->getValue()); $parts = DateTimeParser::parseVCardTime($this->getValue());
$timeStr = ''; $timeStr = '';
// Hour // Hour
@ -84,11 +103,29 @@ class Time extends Text {
// Timezone // Timezone
if (!is_null($parts['timezone'])) { if (!is_null($parts['timezone'])) {
$timeStr.=$parts['timezone']; if ('Z' === $parts['timezone']) {
$timeStr .= 'Z';
} else {
$timeStr .=
preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']);
}
} }
return array($timeStr); return [$timeStr];
} }
/**
* Hydrate data from a XML subtree, as it would appear in a xCard or xCal
* object.
*/
public function setXmlValue(array $value)
{
$value = array_map(
function ($value) {
return str_replace(':', '', $value);
},
$value
);
parent::setXmlValue($value);
}
} }

View file

@ -2,24 +2,18 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
use
Sabre\VObject\Property,
Sabre\VObject\Component,
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\Document;
/** /**
* Unknown property * Unknown property.
* *
* This object represents any properties not recognized by the parser. * This object represents any properties not recognized by the parser.
* This type of value has been introduced by the jCal, jCard specs. * This type of value has been introduced by the jCal, jCard specs.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Unknown extends Text { class Unknown extends Text
{
/** /**
* Returns the value, in the format it should be encoded for json. * Returns the value, in the format it should be encoded for json.
* *
@ -27,10 +21,9 @@ class Unknown extends Text {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
return array($this->getRawMimeDirValue()); return [$this->getRawMimeDirValue()];
} }
/** /**
@ -41,10 +34,8 @@ class Unknown extends Text {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "UNKNOWN"; return 'UNKNOWN';
} }
} }

View file

@ -2,26 +2,27 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property; use Sabre\VObject\Property;
/** /**
* URI property * URI property.
* *
* This object encodes URI values. vCard 2.1 calls these URL. * This object encodes URI values. vCard 2.1 calls these URL.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Uri extends Property { class Uri extends Text
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = null; public $delimiter = '';
/** /**
* Returns the type of value. * Returns the type of value.
@ -31,10 +32,32 @@ class Uri extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'URI';
}
return "URI"; /**
* Returns an iterable list of children.
*
* @return array
*/
public function parameters()
{
$parameters = parent::parameters();
if (!isset($parameters['VALUE']) && in_array($this->name, ['URL', 'PHOTO'])) {
// If we are encoding a URI value, and this URI value has no
// VALUE=URI parameter, we add it anyway.
//
// This is not required by any spec, but both Apple iCal and Apple
// AddressBook (at least in version 10.8) will trip over this if
// this is not set, and so it improves compatibility.
//
// See Issue #227 and #235
$parameters['VALUE'] = new Parameter($this->root, 'VALUE', 'URI');
}
return $parameters;
} }
/** /**
@ -44,12 +67,35 @@ class Uri extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->value = $val; // Normally we don't need to do any type of unescaping for these
// properties, however.. we've noticed that Google Contacts
// specifically escapes the colon (:) with a backslash. While I have
// no clue why they thought that was a good idea, I'm unescaping it
// anyway.
//
// Good thing backslashes are not allowed in urls. Makes it easy to
// assume that a backslash is always intended as an escape character.
if ('URL' === $this->name) {
$regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x';
$matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$newVal = '';
foreach ($matches as $match) {
switch ($match) {
case '\:':
$newVal .= ':';
break;
default:
$newVal .= $match;
break;
}
}
$this->value = $newVal;
} else {
$this->value = strtr($val, ['\,' => ',']);
}
} }
/** /**
@ -57,14 +103,14 @@ class Uri extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
if (is_array($this->value)) { if (is_array($this->value)) {
return $this->value[0]; $value = $this->value[0];
} else { } else {
return $this->value; $value = $this->value;
} }
return strtr($value, [',' => '\,']);
} }
} }

View file

@ -3,23 +3,23 @@
namespace Sabre\VObject\Property; namespace Sabre\VObject\Property;
/** /**
* UtcOffset property * UtcOffset property.
* *
* This object encodes UTC-OFFSET values. * This object encodes UTC-OFFSET values.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class UtcOffset extends Text { class UtcOffset extends Text
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = null; public $delimiter = '';
/** /**
* Returns the type of value. * Returns the type of value.
@ -29,9 +29,42 @@ class UtcOffset extends Text {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'UTC-OFFSET';
}
return "UTC-OFFSET"; /**
* Sets the JSON value, as it would appear in a jCard or jCal object.
*
* The value must always be an array.
*/
public function setJsonValue(array $value)
{
$value = array_map(
function ($value) {
return str_replace(':', '', $value);
},
$value
);
parent::setJsonValue($value);
}
/**
* Returns the value, in the format it should be encoded for JSON.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue()
{
return array_map(
function ($value) {
return substr($value, 0, -2).':'.
substr($value, -2);
},
parent::getJsonValue()
);
} }
} }

View file

@ -2,20 +2,17 @@
namespace Sabre\VObject\Property\VCard; namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser;
/** /**
* Date property * Date property.
* *
* This object encodes vCard DATE values. * This object encodes vCard DATE values.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Date extends DateAndOrTime { class Date extends DateAndOrTime
{
/** /**
* Returns the type of value. * Returns the type of value.
* *
@ -24,10 +21,16 @@ class Date extends DateAndOrTime {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "DATE"; return 'DATE';
} }
/**
* Sets the property as a DateTime object.
*/
public function setDateTime(\DateTimeInterface $dt)
{
$this->value = $dt->format('Ymd');
}
} }

View file

@ -2,27 +2,31 @@
namespace Sabre\VObject\Property\VCard; namespace Sabre\VObject\Property\VCard;
use use DateTime;
Sabre\VObject\DateTimeParser, use DateTimeImmutable;
Sabre\VObject\Property\Text; use DateTimeInterface;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\Property;
use Sabre\Xml;
/** /**
* DateAndOrTime property * DateAndOrTime property.
* *
* This object encodes DATE-AND-OR-TIME values. * This object encodes DATE-AND-OR-TIME values.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class DateAndOrTime extends Text { class DateAndOrTime extends Property
{
/** /**
* Field separator * Field separator.
* *
* @var null|string * @var string
*/ */
public $delimiter = null; public $delimiter = '';
/** /**
* Returns the type of value. * Returns the type of value.
@ -32,10 +36,99 @@ class DateAndOrTime extends Text {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return 'DATE-AND-OR-TIME';
}
return "DATE-AND-OR-TIME"; /**
* Sets a multi-valued property.
*
* You may also specify DateTimeInterface objects here.
*/
public function setParts(array $parts)
{
if (count($parts) > 1) {
throw new \InvalidArgumentException('Only one value allowed');
}
if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) {
$this->setDateTime($parts[0]);
} else {
parent::setParts($parts);
}
}
/**
* Updates the current value.
*
* This may be either a single, or multiple strings in an array.
*
* Instead of strings, you may also use DateTimeInterface here.
*
* @param string|array|DateTimeInterface $value
*/
public function setValue($value)
{
if ($value instanceof DateTimeInterface) {
$this->setDateTime($value);
} else {
parent::setValue($value);
}
}
/**
* Sets the property as a DateTime object.
*/
public function setDateTime(DateTimeInterface $dt)
{
$tz = $dt->getTimeZone();
$isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']);
if ($isUtc) {
$value = $dt->format('Ymd\\THis\\Z');
} else {
// Calculating the offset.
$value = $dt->format('Ymd\\THisO');
}
$this->value = $value;
}
/**
* Returns a date-time value.
*
* Note that if this property contained more than 1 date-time, only the
* first will be returned. To get an array with multiple values, call
* getDateTimes.
*
* If no time was specified, we will always use midnight (in the default
* timezone) as the time.
*
* If parts of the date were omitted, such as the year, we will grab the
* current values for those. So at the time of writing, if the year was
* omitted, we would have filled in 2014.
*
* @return DateTimeImmutable
*/
public function getDateTime()
{
$now = new DateTime();
$tzFormat = 0 === $now->getTimezone()->getOffset($now) ? '\\Z' : 'O';
$nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This'.$tzFormat));
$dateParts = DateTimeParser::parseVCardDateTime($this->getValue());
// This sets all the missing parts to the current date/time.
// So if the year was missing for a birthday, we're making it 'this
// year'.
foreach ($dateParts as $k => $v) {
if (is_null($v)) {
$dateParts[$k] = $nowParts[$k];
}
}
return new DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]");
} }
/** /**
@ -45,8 +138,8 @@ class DateAndOrTime extends Text {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
$parts = DateTimeParser::parseVCardDateTime($this->getValue()); $parts = DateTimeParser::parseVCardDateTime($this->getValue());
$dateStr = ''; $dateStr = '';
@ -60,18 +153,14 @@ class DateAndOrTime extends Text {
// dash. // dash.
$dateStr .= '-'; $dateStr .= '-';
} }
} else { } else {
if (!is_null($parts['month']) || !is_null($parts['date'])) { if (!is_null($parts['month']) || !is_null($parts['date'])) {
// Inserting two dashes // Inserting two dashes
$dateStr .= '--'; $dateStr .= '--';
} }
} }
// Month // Month
if (!is_null($parts['month'])) { if (!is_null($parts['month'])) {
$dateStr .= $parts['month']; $dateStr .= $parts['month'];
@ -79,23 +168,20 @@ class DateAndOrTime extends Text {
// If month and date are set, we need the separator dash. // If month and date are set, we need the separator dash.
$dateStr .= '-'; $dateStr .= '-';
} }
} else { } elseif (isset($parts['date'])) {
if (isset($parts['date'])) {
// If the month is empty, and a date is set, we need a 'empty // If the month is empty, and a date is set, we need a 'empty
// dash' // dash'
$dateStr .= '-'; $dateStr .= '-';
} }
}
// Date // Date
if (!is_null($parts['date'])) { if (!is_null($parts['date'])) {
$dateStr .= $parts['date']; $dateStr .= $parts['date'];
} }
// Early exit if we don't have a time string. // Early exit if we don't have a time string.
if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) { if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) {
return array($dateStr); return [$dateStr];
} }
$dateStr .= 'T'; $dateStr .= 'T';
@ -120,12 +206,10 @@ class DateAndOrTime extends Text {
if (!is_null($parts['second'])) { if (!is_null($parts['second'])) {
$dateStr .= ':'; $dateStr .= ':';
} }
} else { } elseif (isset($parts['second'])) {
if (isset($parts['second'])) {
// Dash for empty minute // Dash for empty minute
$dateStr .= '-'; $dateStr .= '-';
} }
}
// Second // Second
if (!is_null($parts['second'])) { if (!is_null($parts['second'])) {
@ -137,8 +221,147 @@ class DateAndOrTime extends Text {
$dateStr .= $parts['timezone']; $dateStr .= $parts['timezone'];
} }
return array($dateStr); return [$dateStr];
} }
/**
* This method serializes only the value of a property. This is used to
* create xCard or xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
protected function xmlSerializeValue(Xml\Writer $writer)
{
$valueType = strtolower($this->getValueType());
$parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue());
$value = '';
// $d = defined
$d = function ($part) use ($parts) {
return !is_null($parts[$part]);
};
// $r = read
$r = function ($part) use ($parts) {
return $parts[$part];
};
// From the Relax NG Schema.
//
// # 4.3.1
// value-date = element date {
// xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" }
// }
if (($d('year') || $d('month') || $d('date'))
&& (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) {
if ($d('year') && $d('month') && $d('date')) {
$value .= $r('year').$r('month').$r('date');
} elseif ($d('year') && $d('month') && !$d('date')) {
$value .= $r('year').'-'.$r('month');
} elseif (!$d('year') && $d('month')) {
$value .= '--'.$r('month').$r('date');
} elseif (!$d('year') && !$d('month') && $d('date')) {
$value .= '---'.$r('date');
}
// # 4.3.2
// value-time = element time {
// xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)"
// ~ "(Z|[+\-]\d\d(\d\d)?)?" }
// }
} elseif ((!$d('year') && !$d('month') && !$d('date'))
&& ($d('hour') || $d('minute') || $d('second'))) {
if ($d('hour')) {
$value .= $r('hour').$r('minute').$r('second');
} elseif ($d('minute')) {
$value .= '-'.$r('minute').$r('second');
} elseif ($d('second')) {
$value .= '--'.$r('second');
}
$value .= $r('timezone');
// # 4.3.3
// value-date-time = element date-time {
// xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?"
// ~ "(Z|[+\-]\d\d(\d\d)?)?" }
// }
} elseif ($d('date') && $d('hour')) {
if ($d('year') && $d('month') && $d('date')) {
$value .= $r('year').$r('month').$r('date');
} elseif (!$d('year') && $d('month') && $d('date')) {
$value .= '--'.$r('month').$r('date');
} elseif (!$d('year') && !$d('month') && $d('date')) {
$value .= '---'.$r('date');
}
$value .= 'T'.$r('hour').$r('minute').$r('second').
$r('timezone');
}
$writer->writeElement($valueType, $value);
}
/**
* Sets a raw value coming from a mimedir (iCalendar/vCard) file.
*
* This has been 'unfolded', so only 1 line will be passed. Unescaping is
* not yet done, but parameters are not included.
*
* @param string $val
*/
public function setRawMimeDirValue($val)
{
$this->setValue($val);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue()
{
return implode($this->delimiter, $this->getParts());
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* Node::REPAIR - May attempt to automatically repair the problem.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on)
* 2 - An inconsequential issue
* 3 - A severe issue.
*
* @param int $options
*
* @return array
*/
public function validate($options = 0)
{
$messages = parent::validate($options);
$value = $this->getValue();
try {
DateTimeParser::parseVCardDateTime($value);
} catch (InvalidDataException $e) {
$messages[] = [
'level' => 3,
'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property',
'node' => $this,
];
}
return $messages;
}
} }

View file

@ -2,20 +2,17 @@
namespace Sabre\VObject\Property\VCard; namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser;
/** /**
* DateTime property * DateTime property.
* *
* This object encodes DATE-TIME values for vCards. * This object encodes DATE-TIME values for vCards.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class DateTime extends DateAndOrTime { class DateTime extends DateAndOrTime
{
/** /**
* Returns the type of value. * Returns the type of value.
* *
@ -24,10 +21,8 @@ class DateTime extends DateAndOrTime {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "DATE-TIME"; return 'DATE-TIME';
} }
} }

View file

@ -2,20 +2,19 @@
namespace Sabre\VObject\Property\VCard; namespace Sabre\VObject\Property\VCard;
use use Sabre\VObject\Property;
Sabre\VObject\Property;
/** /**
* LanguageTag property * LanguageTag property.
* *
* This object represents LANGUAGE-TAG values as used in vCards. * This object represents LANGUAGE-TAG values as used in vCards.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class LanguageTag extends Property { class LanguageTag extends Property
{
/** /**
* Sets a raw value coming from a mimedir (iCalendar/vCard) file. * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
* *
@ -23,12 +22,10 @@ class LanguageTag extends Property {
* not yet done, but parameters are not included. * not yet done, but parameters are not included.
* *
* @param string $val * @param string $val
* @return void
*/ */
public function setRawMimeDirValue($val) { public function setRawMimeDirValue($val)
{
$this->setValue($val); $this->setValue($val);
} }
/** /**
@ -36,10 +33,9 @@ class LanguageTag extends Property {
* *
* @return string * @return string
*/ */
public function getRawMimeDirValue() { public function getRawMimeDirValue()
{
return $this->value; return $this->getValue();
} }
/** /**
@ -50,10 +46,8 @@ class LanguageTag extends Property {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "LANGUAGE-TAG"; return 'LANGUAGE-TAG';
} }
} }

View file

@ -0,0 +1,30 @@
<?php
namespace Sabre\VObject\Property\VCard;
use Sabre\VObject\Property;
/**
* PhoneNumber property.
*
* This object encodes PHONE-NUMBER values.
*
* @author Christian Kraus <christian@kraus.work>
*/
class PhoneNumber extends Property\Text
{
protected $structuredValues = [];
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType()
{
return 'PHONE-NUMBER';
}
}

View file

@ -2,28 +2,28 @@
namespace Sabre\VObject\Property\VCard; namespace Sabre\VObject\Property\VCard;
use use Sabre\VObject\DateTimeParser;
Sabre\VObject\DateTimeParser, use Sabre\VObject\Property\Text;
Sabre\VObject\Property\Text; use Sabre\Xml;
/** /**
* TimeStamp property * TimeStamp property.
* *
* This object encodes TIMESTAMP values. * This object encodes TIMESTAMP values.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class TimeStamp extends Text { class TimeStamp extends Text
{
/** /**
* In case this is a multi-value property. This string will be used as a * In case this is a multi-value property. This string will be used as a
* delimiter. * delimiter.
* *
* @var string|null * @var string
*/ */
public $delimiter = null; public $delimiter = '';
/** /**
* Returns the type of value. * Returns the type of value.
@ -33,10 +33,9 @@ class TimeStamp extends Text {
* *
* @return string * @return string
*/ */
public function getValueType() { public function getValueType()
{
return "TIMESTAMP"; return 'TIMESTAMP';
} }
/** /**
@ -46,8 +45,8 @@ class TimeStamp extends Text {
* *
* @return array * @return array
*/ */
public function getJsonValue() { public function getJsonValue()
{
$parts = DateTimeParser::parseVCardDateTime($this->getValue()); $parts = DateTimeParser::parseVCardDateTime($this->getValue());
$dateStr = $dateStr =
@ -63,7 +62,20 @@ class TimeStamp extends Text {
$dateStr .= $parts['timezone']; $dateStr .= $parts['timezone'];
} }
return array($dateStr); return [$dateStr];
}
/**
* This method serializes only the value of a property. This is used to
* create xCard or xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
protected function xmlSerializeValue(Xml\Writer $writer)
{
// xCard is the only XML and JSON format that has the same date and time
// format than vCard.
$valueType = strtolower($this->getValueType());
$writer->writeElement($valueType, $this->getValue());
} }
} }

View file

@ -3,17 +3,17 @@
namespace Sabre\VObject; namespace Sabre\VObject;
/** /**
* iCalendar/vCard/jCal/jCard reader object. * iCalendar/vCard/jCal/jCard/xCal/xCard reader object.
* *
* This object provides a few (static) convenience methods to quickly access * This object provides a few (static) convenience methods to quickly access
* the parsers. * the parsers.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Reader { class Reader
{
/** /**
* If this option is passed to the reader, it will be less strict about the * If this option is passed to the reader, it will be less strict about the
* validity of the lines. * validity of the lines.
@ -36,15 +36,17 @@ class Reader {
* *
* @param string|resource $data * @param string|resource $data
* @param int $options * @param int $options
* @param string $charset
*
* @return Document * @return Document
*/ */
static function read($data, $options = 0) { public static function read($data, $options = 0, $charset = 'UTF-8')
{
$parser = new Parser\MimeDir(); $parser = new Parser\MimeDir();
$parser->setCharset($charset);
$result = $parser->parse($data, $options); $result = $parser->parse($data, $options);
return $result; return $result;
} }
/** /**
@ -53,21 +55,41 @@ class Reader {
* The options argument is a bitfield. Pass any of the OPTIONS constant to * The options argument is a bitfield. Pass any of the OPTIONS constant to
* alter the parsers' behaviour. * alter the parsers' behaviour.
* *
* You can either a string, a readable stream, or an array for it's input. * You can either a string, a readable stream, or an array for its input.
* Specifying the array is useful if json_decode was already called on the * Specifying the array is useful if json_decode was already called on the
* input. * input.
* *
* @param string|resource|array $data * @param string|resource|array $data
* @param int $options * @param int $options
* @return Node *
* @return Document
*/ */
static function readJson($data, $options = 0) { public static function readJson($data, $options = 0)
{
$parser = new Parser\Json(); $parser = new Parser\Json();
$result = $parser->parse($data, $options); $result = $parser->parse($data, $options);
return $result; return $result;
} }
/**
* Parses a xCard or xCal object, and returns the top component.
*
* The options argument is a bitfield. Pass any of the OPTIONS constant to
* alter the parsers' behaviour.
*
* You can either supply a string, or a readable stream for input.
*
* @param string|resource $data
* @param int $options
*
* @return Document
*/
public static function readXML($data, $options = 0)
{
$parser = new Parser\XML();
$result = $parser->parse($data, $options);
return $result;
}
} }

View file

@ -0,0 +1,497 @@
<?php
namespace Sabre\VObject\Recur;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use Sabre\VObject\Component;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Settings;
/**
* This class is used to determine new for a recurring event, when the next
* events occur.
*
* This iterator may loop infinitely in the future, therefore it is important
* that if you use this class, you set hard limits for the amount of iterations
* you want to handle.
*
* Note that currently there is not full support for the entire iCalendar
* specification, as it's very complex and contains a lot of permutations
* that's not yet used very often in software.
*
* For the focus has been on features as they actually appear in Calendaring
* software, but this may well get expanded as needed / on demand
*
* The following RRULE properties are supported
* * UNTIL
* * INTERVAL
* * COUNT
* * FREQ=DAILY
* * BYDAY
* * BYHOUR
* * BYMONTH
* * FREQ=WEEKLY
* * BYDAY
* * BYHOUR
* * WKST
* * FREQ=MONTHLY
* * BYMONTHDAY
* * BYDAY
* * BYSETPOS
* * FREQ=YEARLY
* * BYMONTH
* * BYYEARDAY
* * BYWEEKNO
* * BYMONTHDAY (only if BYMONTH is also set)
* * BYDAY (only if BYMONTH is also set)
*
* Anything beyond this is 'undefined', which means that it may get ignored, or
* you may get unexpected results. The effect is that in some applications the
* specified recurrence may look incorrect, or is missing.
*
* The recurrence iterator also does not yet support THISANDFUTURE.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class EventIterator implements \Iterator
{
/**
* Reference timeZone for floating dates and times.
*
* @var DateTimeZone
*/
protected $timeZone;
/**
* True if we're iterating an all-day event.
*
* @var bool
*/
protected $allDay = false;
/**
* Creates the iterator.
*
* There's three ways to set up the iterator.
*
* 1. You can pass a VCALENDAR component and a UID.
* 2. You can pass an array of VEVENTs (all UIDS should match).
* 3. You can pass a single VEVENT component.
*
* Only the second method is recommended. The other 1 and 3 will be removed
* at some point in the future.
*
* The $uid parameter is only required for the first method.
*
* @param Component|array $input
* @param string|null $uid
* @param DateTimeZone $timeZone reference timezone for floating dates and
* times
*/
public function __construct($input, $uid = null, DateTimeZone $timeZone = null)
{
if (is_null($timeZone)) {
$timeZone = new DateTimeZone('UTC');
}
$this->timeZone = $timeZone;
if (is_array($input)) {
$events = $input;
} elseif ($input instanceof VEvent) {
// Single instance mode.
$events = [$input];
} else {
// Calendar + UID mode.
$uid = (string) $uid;
if (!$uid) {
throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor');
}
if (!isset($input->VEVENT)) {
throw new InvalidArgumentException('No events found in this calendar');
}
$events = $input->getByUID($uid);
}
foreach ($events as $vevent) {
if (!isset($vevent->{'RECURRENCE-ID'})) {
$this->masterEvent = $vevent;
} else {
$this->exceptions[
$vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp()
] = true;
$this->overriddenEvents[] = $vevent;
}
}
if (!$this->masterEvent) {
// No base event was found. CalDAV does allow cases where only
// overridden instances are stored.
//
// In this particular case, we're just going to grab the first
// event and use that instead. This may not always give the
// desired result.
if (!count($this->overriddenEvents)) {
throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
}
$this->masterEvent = array_shift($this->overriddenEvents);
}
$this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone);
$this->allDay = !$this->masterEvent->DTSTART->hasTime();
if (isset($this->masterEvent->EXDATE)) {
foreach ($this->masterEvent->EXDATE as $exDate) {
foreach ($exDate->getDateTimes($this->timeZone) as $dt) {
$this->exceptions[$dt->getTimeStamp()] = true;
}
}
}
if (isset($this->masterEvent->DTEND)) {
$this->eventDuration =
$this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() -
$this->startDate->getTimeStamp();
} elseif (isset($this->masterEvent->DURATION)) {
$duration = $this->masterEvent->DURATION->getDateInterval();
$end = clone $this->startDate;
$end = $end->add($duration);
$this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp();
} elseif ($this->allDay) {
$this->eventDuration = 3600 * 24;
} else {
$this->eventDuration = 0;
}
if (isset($this->masterEvent->RDATE)) {
$this->recurIterator = new RDateIterator(
$this->masterEvent->RDATE->getParts(),
$this->startDate
);
} elseif (isset($this->masterEvent->RRULE)) {
$this->recurIterator = new RRuleIterator(
$this->masterEvent->RRULE->getParts(),
$this->startDate
);
} else {
$this->recurIterator = new RRuleIterator(
[
'FREQ' => 'DAILY',
'COUNT' => 1,
],
$this->startDate
);
}
$this->rewind();
if (!$this->valid()) {
throw new NoInstancesException('This recurrence rule does not generate any valid instances');
}
}
/**
* Returns the date for the current position of the iterator.
*
* @return DateTimeImmutable
*/
#[\ReturnTypeWillChange]
public function current()
{
if ($this->currentDate) {
return clone $this->currentDate;
}
}
/**
* This method returns the start date for the current iteration of the
* event.
*
* @return DateTimeImmutable
*/
public function getDtStart()
{
if ($this->currentDate) {
return clone $this->currentDate;
}
}
/**
* This method returns the end date for the current iteration of the
* event.
*
* @return DateTimeImmutable
*/
public function getDtEnd()
{
if (!$this->valid()) {
return;
}
if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) {
return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone);
} else {
$end = clone $this->currentDate;
return $end->modify('+'.$this->eventDuration.' seconds');
}
}
/**
* Returns a VEVENT for the current iterations of the event.
*
* This VEVENT will have a recurrence id, and its DTSTART and DTEND
* altered.
*
* @return VEvent
*/
public function getEventObject()
{
if ($this->currentOverriddenEvent) {
return $this->currentOverriddenEvent;
}
$event = clone $this->masterEvent;
// Ignoring the following block, because PHPUnit's code coverage
// ignores most of these lines, and this messes with our stats.
//
// @codeCoverageIgnoreStart
unset(
$event->RRULE,
$event->EXDATE,
$event->RDATE,
$event->EXRULE,
$event->{'RECURRENCE-ID'}
);
// @codeCoverageIgnoreEnd
$event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating());
if (isset($event->DTEND)) {
$event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating());
}
$recurid = clone $event->DTSTART;
$recurid->name = 'RECURRENCE-ID';
$event->add($recurid);
return $event;
}
/**
* Returns the current position of the iterator.
*
* This is for us simply a 0-based index.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function key()
{
// The counter is always 1 ahead.
return $this->counter - 1;
}
/**
* This is called after next, to see if the iterator is still at a valid
* position, or if it's at the end.
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function valid()
{
if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) {
throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences);
}
return (bool) $this->currentDate;
}
/**
* Sets the iterator back to the starting point.
*
* @return void
*/
#[\ReturnTypeWillChange]
public function rewind()
{
$this->recurIterator->rewind();
// re-creating overridden event index.
$index = [];
foreach ($this->overriddenEvents as $key => $event) {
$stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
$index[$stamp][] = $key;
}
krsort($index);
$this->counter = 0;
$this->overriddenEventsIndex = $index;
$this->currentOverriddenEvent = null;
$this->nextDate = null;
$this->currentDate = clone $this->startDate;
$this->next();
}
/**
* Advances the iterator with one step.
*
* @return void
*/
#[\ReturnTypeWillChange]
public function next()
{
$this->currentOverriddenEvent = null;
++$this->counter;
if ($this->nextDate) {
// We had a stored value.
$nextDate = $this->nextDate;
$this->nextDate = null;
} else {
// We need to ask rruleparser for the next date.
// We need to do this until we find a date that's not in the
// exception list.
do {
if (!$this->recurIterator->valid()) {
$nextDate = null;
break;
}
$nextDate = $this->recurIterator->current();
$this->recurIterator->next();
} while (isset($this->exceptions[$nextDate->getTimeStamp()]));
}
// $nextDate now contains what rrule thinks is the next one, but an
// overridden event may cut ahead.
if ($this->overriddenEventsIndex) {
$offsets = end($this->overriddenEventsIndex);
$timestamp = key($this->overriddenEventsIndex);
$offset = end($offsets);
if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
// Overridden event comes first.
$this->currentOverriddenEvent = $this->overriddenEvents[$offset];
// Putting the rrule next date aside.
$this->nextDate = $nextDate;
$this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
// Ensuring that this item will only be used once.
array_pop($this->overriddenEventsIndex[$timestamp]);
if (!$this->overriddenEventsIndex[$timestamp]) {
array_pop($this->overriddenEventsIndex);
}
// Exit point!
return;
}
}
$this->currentDate = $nextDate;
}
/**
* Quickly jump to a date in the future.
*/
public function fastForward(DateTimeInterface $dateTime)
{
while ($this->valid() && $this->getDtEnd() <= $dateTime) {
$this->next();
}
}
/**
* Returns true if this recurring event never ends.
*
* @return bool
*/
public function isInfinite()
{
return $this->recurIterator->isInfinite();
}
/**
* RRULE parser.
*
* @var RRuleIterator
*/
protected $recurIterator;
/**
* The duration, in seconds, of the master event.
*
* We use this to calculate the DTEND for subsequent events.
*/
protected $eventDuration;
/**
* A reference to the main (master) event.
*
* @var VEVENT
*/
protected $masterEvent;
/**
* List of overridden events.
*
* @var array
*/
protected $overriddenEvents = [];
/**
* Overridden event index.
*
* Key is timestamp, value is the list of indexes of the item in the $overriddenEvent
* property.
*
* @var array
*/
protected $overriddenEventsIndex;
/**
* A list of recurrence-id's that are either part of EXDATE, or are
* overridden.
*
* @var array
*/
protected $exceptions = [];
/**
* Internal event counter.
*
* @var int
*/
protected $counter;
/**
* The very start of the iteration process.
*
* @var DateTimeImmutable
*/
protected $startDate;
/**
* Where we are currently in the iteration process.
*
* @var DateTimeImmutable
*/
protected $currentDate;
/**
* The next date from the rrule parser.
*
* Sometimes we need to temporary store the next date, because an
* overridden event came before.
*
* @var DateTimeImmutable
*/
protected $nextDate;
/**
* The event that overwrites the current iteration.
*
* @var VEVENT
*/
protected $currentOverriddenEvent;
}

View file

@ -0,0 +1,17 @@
<?php
namespace Sabre\VObject\Recur;
use Exception;
/**
* This exception will get thrown when a recurrence rule generated more than
* the maximum number of instances.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class MaxInstancesExceededException extends Exception
{
}

View file

@ -0,0 +1,18 @@
<?php
namespace Sabre\VObject\Recur;
use Exception;
/**
* This exception gets thrown when a recurrence iterator produces 0 instances.
*
* This may happen when every occurrence in a rrule is also in EXDATE.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class NoInstancesException extends Exception
{
}

View file

@ -0,0 +1,175 @@
<?php
namespace Sabre\VObject\Recur;
use DateTimeInterface;
use Iterator;
use Sabre\VObject\DateTimeParser;
/**
* RRuleParser.
*
* This class receives an RRULE string, and allows you to iterate to get a list
* of dates in that recurrence.
*
* For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain
* 5 items, one for each day.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class RDateIterator implements Iterator
{
/**
* Creates the Iterator.
*
* @param string|array $rrule
*/
public function __construct($rrule, DateTimeInterface $start)
{
$this->startDate = $start;
$this->parseRDate($rrule);
$this->currentDate = clone $this->startDate;
}
/* Implementation of the Iterator interface {{{ */
#[\ReturnTypeWillChange]
public function current()
{
if (!$this->valid()) {
return;
}
return clone $this->currentDate;
}
/**
* Returns the current item number.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->counter;
}
/**
* Returns whether the current item is a valid item for the recurrence
* iterator.
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function valid()
{
return $this->counter <= count($this->dates);
}
/**
* Resets the iterator.
*
* @return void
*/
#[\ReturnTypeWillChange]
public function rewind()
{
$this->currentDate = clone $this->startDate;
$this->counter = 0;
}
/**
* Goes on to the next iteration.
*
* @return void
*/
#[\ReturnTypeWillChange]
public function next()
{
++$this->counter;
if (!$this->valid()) {
return;
}
$this->currentDate =
DateTimeParser::parse(
$this->dates[$this->counter - 1],
$this->startDate->getTimezone()
);
}
/* End of Iterator implementation }}} */
/**
* Returns true if this recurring event never ends.
*
* @return bool
*/
public function isInfinite()
{
return false;
}
/**
* This method allows you to quickly go to the next occurrence after the
* specified date.
*/
public function fastForward(DateTimeInterface $dt)
{
while ($this->valid() && $this->currentDate < $dt) {
$this->next();
}
}
/**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*
* @var DateTimeInterface
*/
protected $startDate;
/**
* The date of the current iteration. You can get this by calling
* ->current().
*
* @var DateTimeInterface
*/
protected $currentDate;
/**
* The current item in the list.
*
* You can get this number with the key() method.
*
* @var int
*/
protected $counter = 0;
/* }}} */
/**
* This method receives a string from an RRULE property, and populates this
* class with all the values.
*
* @param string|array $rrule
*/
protected function parseRDate($rdate)
{
if (is_string($rdate)) {
$rdate = explode(',', $rdate);
}
$this->dates = $rdate;
}
/**
* Array with the RRULE dates.
*
* @var array
*/
protected $dates = [];
}

View file

@ -0,0 +1,988 @@
<?php
namespace Sabre\VObject\Recur;
use DateTimeImmutable;
use DateTimeInterface;
use Iterator;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\Property;
/**
* RRuleParser.
*
* This class receives an RRULE string, and allows you to iterate to get a list
* of dates in that recurrence.
*
* For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain
* 5 items, one for each day.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class RRuleIterator implements Iterator
{
/**
* Creates the Iterator.
*
* @param string|array $rrule
*/
public function __construct($rrule, DateTimeInterface $start)
{
$this->startDate = $start;
$this->parseRRule($rrule);
$this->currentDate = clone $this->startDate;
}
/* Implementation of the Iterator interface {{{ */
#[\ReturnTypeWillChange]
public function current()
{
if (!$this->valid()) {
return;
}
return clone $this->currentDate;
}
/**
* Returns the current item number.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->counter;
}
/**
* Returns whether the current item is a valid item for the recurrence
* iterator. This will return false if we've gone beyond the UNTIL or COUNT
* statements.
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function valid()
{
if (null === $this->currentDate) {
return false;
}
if (!is_null($this->count)) {
return $this->counter < $this->count;
}
return is_null($this->until) || $this->currentDate <= $this->until;
}
/**
* Resets the iterator.
*
* @return void
*/
#[\ReturnTypeWillChange]
public function rewind()
{
$this->currentDate = clone $this->startDate;
$this->counter = 0;
}
/**
* Goes on to the next iteration.
*
* @return void
*/
#[\ReturnTypeWillChange]
public function next()
{
// Otherwise, we find the next event in the normal RRULE
// sequence.
switch ($this->frequency) {
case 'hourly':
$this->nextHourly();
break;
case 'daily':
$this->nextDaily();
break;
case 'weekly':
$this->nextWeekly();
break;
case 'monthly':
$this->nextMonthly();
break;
case 'yearly':
$this->nextYearly();
break;
}
++$this->counter;
}
/* End of Iterator implementation }}} */
/**
* Returns true if this recurring event never ends.
*
* @return bool
*/
public function isInfinite()
{
return !$this->count && !$this->until;
}
/**
* This method allows you to quickly go to the next occurrence after the
* specified date.
*/
public function fastForward(DateTimeInterface $dt)
{
while ($this->valid() && $this->currentDate < $dt) {
$this->next();
}
}
/**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*
* @var DateTimeInterface
*/
protected $startDate;
/**
* The date of the current iteration. You can get this by calling
* ->current().
*
* @var DateTimeInterface
*/
protected $currentDate;
/**
* Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
* yearly.
*
* @var string
*/
protected $frequency;
/**
* The number of recurrences, or 'null' if infinitely recurring.
*
* @var int
*/
protected $count;
/**
* The interval.
*
* If for example frequency is set to daily, interval = 2 would mean every
* 2 days.
*
* @var int
*/
protected $interval = 1;
/**
* The last instance of this recurrence, inclusively.
*
* @var DateTimeInterface|null
*/
protected $until;
/**
* Which seconds to recur.
*
* This is an array of integers (between 0 and 60)
*
* @var array
*/
protected $bySecond;
/**
* Which minutes to recur.
*
* This is an array of integers (between 0 and 59)
*
* @var array
*/
protected $byMinute;
/**
* Which hours to recur.
*
* This is an array of integers (between 0 and 23)
*
* @var array
*/
protected $byHour;
/**
* The current item in the list.
*
* You can get this number with the key() method.
*
* @var int
*/
protected $counter = 0;
/**
* Which weekdays to recur.
*
* This is an array of weekdays
*
* This may also be preceded by a positive or negative integer. If present,
* this indicates the nth occurrence of a specific day within the monthly or
* yearly rrule. For instance, -2TU indicates the second-last tuesday of
* the month, or year.
*
* @var array
*/
protected $byDay;
/**
* Which days of the month to recur.
*
* This is an array of days of the months (1-31). The value can also be
* negative. -5 for instance means the 5th last day of the month.
*
* @var array
*/
protected $byMonthDay;
/**
* Which days of the year to recur.
*
* This is an array with days of the year (1 to 366). The values can also
* be negative. For instance, -1 will always represent the last day of the
* year. (December 31st).
*
* @var array
*/
protected $byYearDay;
/**
* Which week numbers to recur.
*
* This is an array of integers from 1 to 53. The values can also be
* negative. -1 will always refer to the last week of the year.
*
* @var array
*/
protected $byWeekNo;
/**
* Which months to recur.
*
* This is an array of integers from 1 to 12.
*
* @var array
*/
protected $byMonth;
/**
* Which items in an existing st to recur.
*
* These numbers work together with an existing by* rule. It specifies
* exactly which items of the existing by-rule to filter.
*
* Valid values are 1 to 366 and -1 to -366. As an example, this can be
* used to recur the last workday of the month.
*
* This would be done by setting frequency to 'monthly', byDay to
* 'MO,TU,WE,TH,FR' and bySetPos to -1.
*
* @var array
*/
protected $bySetPos;
/**
* When the week starts.
*
* @var string
*/
protected $weekStart = 'MO';
/* Functions that advance the iterator {{{ */
/**
* Does the processing for advancing the iterator for hourly frequency.
*/
protected function nextHourly()
{
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours');
}
/**
* Does the processing for advancing the iterator for daily frequency.
*/
protected function nextDaily()
{
if (!$this->byHour && !$this->byDay) {
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' days');
return;
}
$recurrenceHours = [];
if (!empty($this->byHour)) {
$recurrenceHours = $this->getHours();
}
$recurrenceDays = [];
if (!empty($this->byDay)) {
$recurrenceDays = $this->getDays();
}
$recurrenceMonths = [];
if (!empty($this->byMonth)) {
$recurrenceMonths = $this->getMonths();
}
do {
if ($this->byHour) {
if ('23' == $this->currentDate->format('G')) {
// to obey the interval rule
$this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days');
}
$this->currentDate = $this->currentDate->modify('+1 hours');
} else {
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' days');
}
// Current month of the year
$currentMonth = $this->currentDate->format('n');
// Current day of the week
$currentDay = $this->currentDate->format('w');
// Current hour of the day
$currentHour = $this->currentDate->format('G');
} while (
($this->byDay && !in_array($currentDay, $recurrenceDays)) ||
($this->byHour && !in_array($currentHour, $recurrenceHours)) ||
($this->byMonth && !in_array($currentMonth, $recurrenceMonths))
);
}
/**
* Does the processing for advancing the iterator for weekly frequency.
*/
protected function nextWeekly()
{
if (!$this->byHour && !$this->byDay) {
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks');
return;
}
$recurrenceHours = [];
if ($this->byHour) {
$recurrenceHours = $this->getHours();
}
$recurrenceDays = [];
if ($this->byDay) {
$recurrenceDays = $this->getDays();
}
// First day of the week:
$firstDay = $this->dayMap[$this->weekStart];
do {
if ($this->byHour) {
$this->currentDate = $this->currentDate->modify('+1 hours');
} else {
$this->currentDate = $this->currentDate->modify('+1 days');
}
// Current day of the week
$currentDay = (int) $this->currentDate->format('w');
// Current hour of the day
$currentHour = (int) $this->currentDate->format('G');
// We need to roll over to the next week
if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) {
$this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks');
// 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.
if ($this->currentDate->format('w') != $firstDay) {
$this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]);
}
}
// We have a match
} while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
}
/**
* Does the processing for advancing the iterator for monthly frequency.
*/
protected function nextMonthly()
{
$currentDayOfMonth = $this->currentDate->format('j');
if (!$this->byMonthDay && !$this->byDay) {
// If the current day is higher than the 28th, rollover can
// occur to the next month. We Must skip these invalid
// entries.
if ($currentDayOfMonth < 29) {
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' months');
} else {
$increase = 0;
do {
++$increase;
$tempDate = clone $this->currentDate;
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months');
} while ($tempDate->format('j') != $currentDayOfMonth);
$this->currentDate = $tempDate;
}
return;
}
$occurrence = -1;
while (true) {
$occurrences = $this->getMonthlyOccurrences();
foreach ($occurrences as $occurrence) {
// The first occurrence thats higher than the current
// day of the month wins.
if ($occurrence > $currentDayOfMonth) {
break 2;
}
}
// If we made it all the way here, it means there were no
// valid occurrences, and we need to advance to the next
// month.
//
// This line does not currently work in hhvm. Temporary workaround
// follows:
// $this->currentDate->modify('first day of this month');
$this->currentDate = new DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone());
// end of workaround
$this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months');
// This goes to 0 because we need to start counting at the
// beginning.
$currentDayOfMonth = 0;
// For some reason the "until" parameter was not being used here,
// that's why the workaround of the 10000 year bug was needed at all
// let's stop it before the "until" parameter date
if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) {
return;
}
// To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply
// stop at 9999-12-31. Looks like the year 10000 problem is not solved in php ....
if ($this->currentDate->getTimestamp() > 253402300799) {
$this->currentDate = null;
return;
}
}
$this->currentDate = $this->currentDate->setDate(
(int) $this->currentDate->format('Y'),
(int) $this->currentDate->format('n'),
(int) $occurrence
);
}
/**
* Does the processing for advancing the iterator for yearly frequency.
*/
protected function nextYearly()
{
$currentMonth = $this->currentDate->format('n');
$currentYear = $this->currentDate->format('Y');
$currentDayOfMonth = $this->currentDate->format('j');
// No sub-rules, so we just advance by year
if (empty($this->byMonth)) {
// Unless it was a leap day!
if (2 == $currentMonth && 29 == $currentDayOfMonth) {
$counter = 0;
do {
++$counter;
// Here we increase the year count by the interval, until
// we hit a date that's also in a leap year.
//
// We could just find the next interval that's dividable by
// 4, but that would ignore the rule that there's no leap
// year every year that's dividable by a 100, but not by
// 400. (1800, 1900, 2100). So we just rely on the datetime
// functions instead.
$nextDate = clone $this->currentDate;
$nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years');
} while (2 != $nextDate->format('n'));
$this->currentDate = $nextDate;
return;
}
if (null !== $this->byWeekNo) { // byWeekNo is an array with values from -53 to -1, or 1 to 53
$dayOffsets = [];
if ($this->byDay) {
foreach ($this->byDay as $byDay) {
$dayOffsets[] = $this->dayMap[$byDay];
}
} else { // default is Monday
$dayOffsets[] = 1;
}
$currentYear = $this->currentDate->format('Y');
while (true) {
$checkDates = [];
// loop through all WeekNo and Days to check all the combinations
foreach ($this->byWeekNo as $byWeekNo) {
foreach ($dayOffsets as $dayOffset) {
$date = clone $this->currentDate;
$date = $date->setISODate($currentYear, $byWeekNo, $dayOffset);
if ($date > $this->currentDate) {
$checkDates[] = $date;
}
}
}
if (count($checkDates) > 0) {
$this->currentDate = min($checkDates);
return;
}
// if there is no date found, check the next year
$currentYear += $this->interval;
}
}
if (null !== $this->byYearDay) { // byYearDay is an array with values from -366 to -1, or 1 to 366
$dayOffsets = [];
if ($this->byDay) {
foreach ($this->byDay as $byDay) {
$dayOffsets[] = $this->dayMap[$byDay];
}
} else { // default is Monday-Sunday
$dayOffsets = [1, 2, 3, 4, 5, 6, 7];
}
$currentYear = $this->currentDate->format('Y');
while (true) {
$checkDates = [];
// loop through all YearDay and Days to check all the combinations
foreach ($this->byYearDay as $byYearDay) {
$date = clone $this->currentDate;
$date = $date->setDate($currentYear, 1, 1);
if ($byYearDay > 0) {
$date = $date->add(new \DateInterval('P'.$byYearDay.'D'));
} else {
$date = $date->sub(new \DateInterval('P'.abs($byYearDay).'D'));
}
if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) {
$checkDates[] = $date;
}
}
if (count($checkDates) > 0) {
$this->currentDate = min($checkDates);
return;
}
// if there is no date found, check the next year
$currentYear += $this->interval;
}
}
// The easiest form
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' years');
return;
}
$currentMonth = $this->currentDate->format('n');
$currentYear = $this->currentDate->format('Y');
$currentDayOfMonth = $this->currentDate->format('j');
$advancedToNewMonth = false;
// If we got a byDay or getMonthDay filter, we must first expand
// further.
if ($this->byDay || $this->byMonthDay) {
$occurrence = -1;
while (true) {
$occurrences = $this->getMonthlyOccurrences();
foreach ($occurrences as $occurrence) {
// The first occurrence that's higher than the current
// day of the month wins.
// If we advanced to the next month or year, the first
// occurrence is always correct.
if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
break 2;
}
}
// If we made it here, it means we need to advance to
// the next month or year.
$currentDayOfMonth = 1;
$advancedToNewMonth = true;
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
);
}
// If we made it here, it means we got a valid occurrence
$this->currentDate = $this->currentDate->setDate(
(int) $currentYear,
(int) $currentMonth,
(int) $occurrence
);
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
);
return;
}
}
/* }}} */
/**
* This method receives a string from an RRULE property, and populates this
* class with all the values.
*
* @param string|array $rrule
*/
protected function parseRRule($rrule)
{
if (is_string($rrule)) {
$rrule = Property\ICalendar\Recur::stringToArray($rrule);
}
foreach ($rrule as $key => $value) {
$key = strtoupper($key);
switch ($key) {
case 'FREQ':
$value = strtolower($value);
if (!in_array(
$value,
['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly']
)) {
throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value));
}
$this->frequency = $value;
break;
case 'UNTIL':
$this->until = DateTimeParser::parse($value, $this->startDate->getTimezone());
// In some cases events are generated with an UNTIL=
// parameter before the actual start of the event.
//
// Not sure why this is happening. We assume that the
// intention was that the event only recurs once.
//
// So we are modifying the parameter so our code doesn't
// break.
if ($this->until < $this->startDate) {
$this->until = $this->startDate;
}
break;
case 'INTERVAL':
case 'COUNT':
$val = (int) $value;
if ($val < 1) {
throw new InvalidDataException(strtoupper($key).' in RRULE must be a positive integer!');
}
$key = strtolower($key);
$this->$key = $val;
break;
case 'BYSECOND':
$this->bySecond = (array) $value;
break;
case 'BYMINUTE':
$this->byMinute = (array) $value;
break;
case 'BYHOUR':
$this->byHour = (array) $value;
break;
case 'BYDAY':
$value = (array) $value;
foreach ($value as $part) {
if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) {
throw new InvalidDataException('Invalid part in BYDAY clause: '.$part);
}
}
$this->byDay = $value;
break;
case 'BYMONTHDAY':
$this->byMonthDay = (array) $value;
break;
case 'BYYEARDAY':
$this->byYearDay = (array) $value;
foreach ($this->byYearDay as $byYearDay) {
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!');
}
}
break;
case 'BYWEEKNO':
$this->byWeekNo = (array) $value;
foreach ($this->byWeekNo as $byWeekNo) {
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!');
}
}
break;
case 'BYMONTH':
$this->byMonth = (array) $value;
foreach ($this->byMonth as $byMonth) {
if (!is_numeric($byMonth) || (int) $byMonth < 1 || (int) $byMonth > 12) {
throw new InvalidDataException('BYMONTH in RRULE must have value(s) between 1 and 12!');
}
}
break;
case 'BYSETPOS':
$this->bySetPos = (array) $value;
break;
case 'WKST':
$this->weekStart = strtoupper($value);
break;
default:
throw new InvalidDataException('Not supported: '.strtoupper($key));
}
}
}
/**
* Mappings between the day number and english day name.
*
* @var array
*/
protected $dayNames = [
0 => 'Sunday',
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
];
/**
* Returns all the occurrences for a monthly frequency with a 'byDay' or
* 'byMonthDay' expansion for the current month.
*
* The returned list is an array of integers with the day of month (1-31).
*
* @return array
*/
protected function getMonthlyOccurrences()
{
$startDate = clone $this->currentDate;
$byDayResults = [];
// Our strategy is to simply go through the byDays, advance the date to
// that point and add it to the results.
if ($this->byDay) {
foreach ($this->byDay as $day) {
$dayName = $this->dayNames[$this->dayMap[substr($day, -2)]];
// Dayname will be something like 'wednesday'. Now we need to find
// all wednesdays in this month.
$dayHits = [];
// workaround for missing 'first day of the month' support in hhvm
$checkDate = new \DateTime($startDate->format('Y-m-1'));
// workaround modify always advancing the date even if the current day is a $dayName in hhvm
if ($checkDate->format('l') !== $dayName) {
$checkDate = $checkDate->modify($dayName);
}
do {
$dayHits[] = $checkDate->format('j');
$checkDate = $checkDate->modify('next '.$dayName);
} while ($checkDate->format('n') === $startDate->format('n'));
// So now we have 'all wednesdays' for month. It is however
// possible that the user only really wanted the 1st, 2nd or last
// wednesday.
if (strlen($day) > 2) {
$offset = (int) substr($day, 0, -2);
if ($offset > 0) {
// It is possible that the day does not exist, such as a
// 5th or 6th wednesday of the month.
if (isset($dayHits[$offset - 1])) {
$byDayResults[] = $dayHits[$offset - 1];
}
} else {
// if it was negative we count from the end of the array
// might not exist, fx. -5th tuesday
if (isset($dayHits[count($dayHits) + $offset])) {
$byDayResults[] = $dayHits[count($dayHits) + $offset];
}
}
} else {
// There was no counter (first, second, last wednesdays), so we
// just need to add the all to the list).
$byDayResults = array_merge($byDayResults, $dayHits);
}
}
}
$byMonthDayResults = [];
if ($this->byMonthDay) {
foreach ($this->byMonthDay as $monthDay) {
// Removing values that are out of range for this month
if ($monthDay > $startDate->format('t') ||
$monthDay < 0 - $startDate->format('t')) {
continue;
}
if ($monthDay > 0) {
$byMonthDayResults[] = $monthDay;
} else {
// Negative values
$byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
}
}
}
// If there was just byDay or just byMonthDay, they just specify our
// (almost) final list. If both were provided, then byDay limits the
// list.
if ($this->byMonthDay && $this->byDay) {
$result = array_intersect($byMonthDayResults, $byDayResults);
} elseif ($this->byMonthDay) {
$result = $byMonthDayResults;
} else {
$result = $byDayResults;
}
$result = array_unique($result);
sort($result, SORT_NUMERIC);
// The last thing that needs checking is the BYSETPOS. If it's set, it
// means only certain items in the set survive the filter.
if (!$this->bySetPos) {
return $result;
}
$filteredResult = [];
foreach ($this->bySetPos as $setPos) {
if ($setPos < 0) {
$setPos = count($result) + ($setPos + 1);
}
if (isset($result[$setPos - 1])) {
$filteredResult[] = $result[$setPos - 1];
}
}
sort($filteredResult, SORT_NUMERIC);
return $filteredResult;
}
/**
* Simple mapping from iCalendar day names to day numbers.
*
* @var array
*/
protected $dayMap = [
'SU' => 0,
'MO' => 1,
'TU' => 2,
'WE' => 3,
'TH' => 4,
'FR' => 5,
'SA' => 6,
];
protected function getHours()
{
$recurrenceHours = [];
foreach ($this->byHour as $byHour) {
$recurrenceHours[] = $byHour;
}
return $recurrenceHours;
}
protected function getDays()
{
$recurrenceDays = [];
foreach ($this->byDay as $byDay) {
// The day may be preceded with a positive (+n) or
// negative (-n) integer. However, this does not make
// sense in 'weekly' so we ignore it here.
$recurrenceDays[] = $this->dayMap[substr($byDay, -2)];
}
return $recurrenceDays;
}
protected function getMonths()
{
$recurrenceMonths = [];
foreach ($this->byMonth as $byMonth) {
$recurrenceMonths[] = $byMonth;
}
return $recurrenceMonths;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Sabre\VObject;
/**
* This class provides a list of global defaults for vobject.
*
* Some of these started to appear in various classes, so it made a bit more
* sense to centralize them, so it's easier for user to find and change these.
*
* The global nature of them does mean that changing the settings for one
* instance has a global influence.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Settings
{
/**
* The minimum date we accept for various calculations with dates, such as
* recurrences.
*
* The choice of 1900 is pretty arbitrary, but it covers most common
* use-cases. In particular, it covers birthdates for virtually everyone
* alive on earth, which is less than 5 people at the time of writing.
*/
public static $minDate = '1900-01-01';
/**
* The maximum date we accept for various calculations with dates, such as
* recurrences.
*
* The choice of 2100 is pretty arbitrary, but should cover most
* appointments made for many years to come.
*/
public static $maxDate = '2100-01-01';
/**
* The maximum number of recurrences that will be generated.
*
* This setting limits the maximum of recurring events that this library
* generates in its recurrence iterators.
*
* This is a security measure. Without this, it would be possible to craft
* specific events that recur many, many times, potentially DDOSing the
* server.
*
* The default (3500) allows creation of a daily event that goes on for 10
* years, which is hopefully long enough for most.
*
* Set this value to -1 to disable this control altogether.
*/
public static $maxRecurrences = 3500;
}

View file

@ -2,12 +2,11 @@
namespace Sabre\VObject\Splitter; namespace Sabre\VObject\Splitter;
use use Sabre\VObject;
Sabre\VObject, use Sabre\VObject\Component\VCalendar;
Sabre\VObject\Component\VCalendar;
/** /**
* Splitter * Splitter.
* *
* This class is responsible for splitting up iCalendar objects. * This class is responsible for splitting up iCalendar objects.
* *
@ -15,40 +14,42 @@ use
* calendar-objects inside. Objects with identical UID's will be combined into * calendar-objects inside. Objects with identical UID's will be combined into
* a single object. * a single object.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Dominik Tobschall * @author Dominik Tobschall (http://tobschall.de/)
* @author Armin Hackmann * @author Armin Hackmann
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class ICalendar implements SplitterInterface { class ICalendar implements SplitterInterface
{
/** /**
* Timezones * Timezones.
* *
* @var array * @var array
*/ */
protected $vtimezones = array(); protected $vtimezones = [];
/** /**
* iCalendar objects * iCalendar objects.
* *
* @var array * @var array
*/ */
protected $objects = array(); protected $objects = [];
/** /**
* Constructor * Constructor.
* *
* The splitter should receive an readable file stream as it's input. * The splitter should receive an readable file stream as its input.
* *
* @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, $options = 0) { public function __construct($input, $options = 0)
{
$data = VObject\Reader::read($input, $options); $data = VObject\Reader::read($input, $options);
$vtimezones = array();
$components = array(); if (!$data instanceof VObject\Component\VCalendar) {
throw new VObject\ParseException('Supplied input could not be parsed as VCALENDAR.');
}
foreach ($data->children() as $component) { foreach ($data->children() as $component) {
if (!$component instanceof VObject\Component) { if (!$component instanceof VObject\Component) {
@ -56,18 +57,16 @@ class ICalendar implements SplitterInterface {
} }
// Get all timezones // Get all timezones
if ($component->name === 'VTIMEZONE') { if ('VTIMEZONE' === $component->name) {
$this->vtimezones[(string) $component->TZID] = $component; $this->vtimezones[(string) $component->TZID] = $component;
continue; continue;
} }
// Get component UID for recurring Events search // Get component UID for recurring Events search
if($component->UID) { if (!$component->UID) {
$uid = (string)$component->UID; $component->UID = sha1(microtime()).'-vobjectimport';
} else {
// Generating a random UID
$uid = sha1(microtime()) . '-vobjectimport';
} }
$uid = (string) $component->UID;
// Take care of recurring events // Take care of recurring events
if (!array_key_exists($uid, $this->objects)) { if (!array_key_exists($uid, $this->objects)) {
@ -76,7 +75,6 @@ class ICalendar implements SplitterInterface {
$this->objects[$uid]->add(clone $component); $this->objects[$uid]->add(clone $component);
} }
} }
/** /**
@ -85,12 +83,11 @@ class ICalendar implements SplitterInterface {
* *
* When the end is reached, null will be returned. * When the end is reached, null will be returned.
* *
* @return Sabre\VObject\Component|null * @return \Sabre\VObject\Component|null
*/ */
public function getNext() { public function getNext()
{
if ($object = array_shift($this->objects)) { if ($object = array_shift($this->objects)) {
// create our baseobject // create our baseobject
$object->version = '2.0'; $object->version = '2.0';
$object->prodid = '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN'; $object->prodid = '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN';
@ -102,13 +99,8 @@ class ICalendar implements SplitterInterface {
} }
return $object; return $object;
} else { } else {
return;
return null;
} }
} }
} }

View file

@ -3,7 +3,7 @@
namespace Sabre\VObject\Splitter; namespace Sabre\VObject\Splitter;
/** /**
* VObject splitter * VObject splitter.
* *
* The splitter is responsible for reading a large vCard or iCalendar object, * The splitter is responsible for reading a large vCard or iCalendar object,
* and splitting it into multiple objects. * and splitting it into multiple objects.
@ -11,20 +11,20 @@ namespace Sabre\VObject\Splitter;
* This is for example for Card and CalDAV, which require every event and vcard * This is for example for Card and CalDAV, which require every event and vcard
* to exist in their own objects, instead of one large one. * to exist in their own objects, instead of one large one.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Dominik Tobschall * @author Dominik Tobschall (http://tobschall.de/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
interface SplitterInterface { interface SplitterInterface
{
/** /**
* Constructor * Constructor.
* *
* The splitter should receive an readable file stream as it's input. * The splitter should receive an readable file stream as its input.
* *
* @param resource $input * @param resource $input
*/ */
function __construct($input); public function __construct($input);
/** /**
* Every time getNext() is called, a new object will be parsed, until we * Every time getNext() is called, a new object will be parsed, until we
@ -32,8 +32,7 @@ interface SplitterInterface {
* *
* When the end is reached, null will be returned. * When the end is reached, null will be returned.
* *
* @return Sabre\VObject\Component|null * @return \Sabre\VObject\Component|null
*/ */
function getNext(); public function getNext();
} }

View file

@ -2,12 +2,11 @@
namespace Sabre\VObject\Splitter; namespace Sabre\VObject\Splitter;
use use Sabre\VObject;
Sabre\VObject, use Sabre\VObject\Parser\MimeDir;
Sabre\VObject\Parser\MimeDir;
/** /**
* Splitter * Splitter.
* *
* This class is responsible for splitting up VCard objects. * This class is responsible for splitting up VCard objects.
* *
@ -15,40 +14,39 @@ use
* class checks for BEGIN:VCARD and END:VCARD and parses each encountered * class checks for BEGIN:VCARD and END:VCARD and parses each encountered
* component individually. * component individually.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Dominik Tobschall * @author Dominik Tobschall (http://tobschall.de/)
* @author Armin Hackmann * @author Armin Hackmann
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VCard implements SplitterInterface { class VCard implements SplitterInterface
{
/** /**
* File handle * File handle.
* *
* @var resource * @var resource
*/ */
protected $input; protected $input;
/** /**
* Persistent parser * Persistent parser.
* *
* @var MimeDir * @var MimeDir
*/ */
protected $parser; protected $parser;
/** /**
* Constructor * Constructor.
* *
* The splitter should receive an readable file stream as it's input. * The splitter should receive an readable file stream as its input.
* *
* @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, $options = 0) { public function __construct($input, $options = 0)
{
$this->input = $input; $this->input = $input;
$this->parser = new MimeDir($input, $options); $this->parser = new MimeDir($input, $options);
} }
/** /**
@ -57,18 +55,20 @@ class VCard implements SplitterInterface {
* *
* When the end is reached, null will be returned. * When the end is reached, null will be returned.
* *
* @return Sabre\VObject\Component|null * @return \Sabre\VObject\Component|null
*/ */
public function getNext() { public function getNext()
{
try { try {
$object = $this->parser->parse(); $object = $this->parser->parse();
if (!$object instanceof VObject\Component\VCard) {
throw new VObject\ParseException('The supplied input contained non-VCARD data.');
}
} catch (VObject\EofException $e) { } catch (VObject\EofException $e) {
return null; return;
} }
return $object; return $object;
} }
} }

View file

@ -5,32 +5,27 @@ namespace Sabre\VObject;
/** /**
* Useful utilities for working with various strings. * Useful utilities for working with various strings.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class StringUtil { class StringUtil
{
/** /**
* Returns true or false depending on if a string is valid UTF-8 * Returns true or false depending on if a string is valid UTF-8.
* *
* @param string $str * @param string $str
*
* @return bool * @return bool
*/ */
static function isUTF8($str) { public static function isUTF8($str)
{
// First check.. mb_check_encoding
if (!mb_check_encoding($str, 'UTF-8')) {
return false;
}
// Control characters // Control characters
if (preg_match('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', $str)) { if (preg_match('%[\x00-\x08\x0B-\x0C\x0E\x0F]%', $str)) {
return false; return false;
} }
return true; return (bool) preg_match('%%u', $str);
} }
/** /**
@ -40,22 +35,28 @@ class StringUtil {
* may be expanded upon if we receive other examples. * may be expanded upon if we receive other examples.
* *
* @param string $str * @param string $str
*
* @return string * @return string
*/ */
static function convertToUTF8($str) { public static function convertToUTF8($str)
{
$encoding = mb_detect_encoding($str, ['UTF-8', 'ISO-8859-1', 'WINDOWS-1252'], true);
$encoding = mb_detect_encoding($str , array('UTF-8','ISO-8859-1'), true); switch ($encoding) {
case 'ISO-8859-1':
if ($encoding === 'ISO-8859-1') {
$newStr = utf8_encode($str); $newStr = utf8_encode($str);
} else { break;
/* Unreachable code. Not sure yet how we can improve this
* situation.
case 'WINDOWS-1252' :
$newStr = iconv('cp1252', 'UTF-8', $str);
break;
*/
default:
$newStr = $str; $newStr = $str;
} }
// Removing any control characters // Removing any control characters
return (preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', '', $newStr)); return preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', '', $newStr);
} }
} }

View file

@ -2,313 +2,158 @@
namespace Sabre\VObject; namespace Sabre\VObject;
use DateTimeZone;
use InvalidArgumentException;
use Sabre\VObject\TimezoneGuesser\FindFromOffset;
use Sabre\VObject\TimezoneGuesser\FindFromTimezoneIdentifier;
use Sabre\VObject\TimezoneGuesser\FindFromTimezoneMap;
use Sabre\VObject\TimezoneGuesser\GuessFromLicEntry;
use Sabre\VObject\TimezoneGuesser\GuessFromMsTzId;
use Sabre\VObject\TimezoneGuesser\TimezoneFinder;
use Sabre\VObject\TimezoneGuesser\TimezoneGuesser;
/** /**
* Time zone name translation * Time zone name translation.
* *
* This file translates well-known time zone names into "Olson database" time zone names. * This file translates well-known time zone names into "Olson database" time zone names.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Frank Edelhaeuser (fedel@users.sourceforge.net) * @author Frank Edelhaeuser (fedel@users.sourceforge.net)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class TimeZoneUtil { class TimeZoneUtil
{
/** @var self */
private static $instance = null;
public static $map = array( /** @var TimezoneGuesser[] */
private $timezoneGuessers = [];
// from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html /** @var TimezoneFinder[] */
// snapshot taken on 2012/01/16 private $timezoneFinders = [];
// windows private function __construct()
'AUS Central Standard Time'=>'Australia/Darwin', {
'AUS Eastern Standard Time'=>'Australia/Sydney', $this->addGuesser('lic', new GuessFromLicEntry());
'Afghanistan Standard Time'=>'Asia/Kabul', $this->addGuesser('msTzId', new GuessFromMsTzId());
'Alaskan Standard Time'=>'America/Anchorage', $this->addFinder('tzid', new FindFromTimezoneIdentifier());
'Arab Standard Time'=>'Asia/Riyadh', $this->addFinder('tzmap', new FindFromTimezoneMap());
'Arabian Standard Time'=>'Asia/Dubai', $this->addFinder('offset', new FindFromOffset());
'Arabic Standard Time'=>'Asia/Baghdad', }
'Argentina Standard Time'=>'America/Buenos_Aires',
'Armenian Standard Time'=>'Asia/Yerevan',
'Atlantic Standard Time'=>'America/Halifax',
'Azerbaijan Standard Time'=>'Asia/Baku',
'Azores Standard Time'=>'Atlantic/Azores',
'Bangladesh Standard Time'=>'Asia/Dhaka',
'Canada Central Standard Time'=>'America/Regina',
'Cape Verde Standard Time'=>'Atlantic/Cape_Verde',
'Caucasus Standard Time'=>'Asia/Yerevan',
'Cen. Australia Standard Time'=>'Australia/Adelaide',
'Central America Standard Time'=>'America/Guatemala',
'Central Asia Standard Time'=>'Asia/Almaty',
'Central Brazilian Standard Time'=>'America/Cuiaba',
'Central Europe Standard Time'=>'Europe/Budapest',
'Central European Standard Time'=>'Europe/Warsaw',
'Central Pacific Standard Time'=>'Pacific/Guadalcanal',
'Central Standard Time'=>'America/Chicago',
'Central Standard Time (Mexico)'=>'America/Mexico_City',
'China Standard Time'=>'Asia/Shanghai',
'Dateline Standard Time'=>'Etc/GMT+12',
'E. Africa Standard Time'=>'Africa/Nairobi',
'E. Australia Standard Time'=>'Australia/Brisbane',
'E. Europe Standard Time'=>'Europe/Minsk',
'E. South America Standard Time'=>'America/Sao_Paulo',
'Eastern Standard Time'=>'America/New_York',
'Egypt Standard Time'=>'Africa/Cairo',
'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg',
'FLE Standard Time'=>'Europe/Kiev',
'Fiji Standard Time'=>'Pacific/Fiji',
'GMT Standard Time'=>'Europe/London',
'GTB Standard Time'=>'Europe/Istanbul',
'Georgian Standard Time'=>'Asia/Tbilisi',
'Greenland Standard Time'=>'America/Godthab',
'Greenwich Standard Time'=>'Atlantic/Reykjavik',
'Hawaiian Standard Time'=>'Pacific/Honolulu',
'India Standard Time'=>'Asia/Calcutta',
'Iran Standard Time'=>'Asia/Tehran',
'Israel Standard Time'=>'Asia/Jerusalem',
'Jordan Standard Time'=>'Asia/Amman',
'Kamchatka Standard Time'=>'Asia/Kamchatka',
'Korea Standard Time'=>'Asia/Seoul',
'Magadan Standard Time'=>'Asia/Magadan',
'Mauritius Standard Time'=>'Indian/Mauritius',
'Mexico Standard Time'=>'America/Mexico_City',
'Mexico Standard Time 2'=>'America/Chihuahua',
'Mid-Atlantic Standard Time'=>'Etc/GMT-2',
'Middle East Standard Time'=>'Asia/Beirut',
'Montevideo Standard Time'=>'America/Montevideo',
'Morocco Standard Time'=>'Africa/Casablanca',
'Mountain Standard Time'=>'America/Denver',
'Mountain Standard Time (Mexico)'=>'America/Chihuahua',
'Myanmar Standard Time'=>'Asia/Rangoon',
'N. Central Asia Standard Time'=>'Asia/Novosibirsk',
'Namibia Standard Time'=>'Africa/Windhoek',
'Nepal Standard Time'=>'Asia/Katmandu',
'New Zealand Standard Time'=>'Pacific/Auckland',
'Newfoundland Standard Time'=>'America/St_Johns',
'North Asia East Standard Time'=>'Asia/Irkutsk',
'North Asia Standard Time'=>'Asia/Krasnoyarsk',
'Pacific SA Standard Time'=>'America/Santiago',
'Pacific Standard Time'=>'America/Los_Angeles',
'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel',
'Pakistan Standard Time'=>'Asia/Karachi',
'Paraguay Standard Time'=>'America/Asuncion',
'Romance Standard Time'=>'Europe/Paris',
'Russian Standard Time'=>'Europe/Moscow',
'SA Eastern Standard Time'=>'America/Cayenne',
'SA Pacific Standard Time'=>'America/Bogota',
'SA Western Standard Time'=>'America/La_Paz',
'SE Asia Standard Time'=>'Asia/Bangkok',
'Samoa Standard Time'=>'Pacific/Apia',
'Singapore Standard Time'=>'Asia/Singapore',
'South Africa Standard Time'=>'Africa/Johannesburg',
'Sri Lanka Standard Time'=>'Asia/Colombo',
'Syria Standard Time'=>'Asia/Damascus',
'Taipei Standard Time'=>'Asia/Taipei',
'Tasmania Standard Time'=>'Australia/Hobart',
'Tokyo Standard Time'=>'Asia/Tokyo',
'Tonga Standard Time'=>'Pacific/Tongatapu',
'US Eastern Standard Time'=>'America/Indianapolis',
'US Mountain Standard Time'=>'America/Phoenix',
'UTC+12'=>'Etc/GMT-12',
'UTC-02'=>'Etc/GMT+2',
'UTC-11'=>'Etc/GMT+11',
'Ulaanbaatar Standard Time'=>'Asia/Ulaanbaatar',
'Venezuela Standard Time'=>'America/Caracas',
'Vladivostok Standard Time'=>'Asia/Vladivostok',
'W. Australia Standard Time'=>'Australia/Perth',
'W. Central Africa Standard Time'=>'Africa/Lagos',
'W. Europe Standard Time'=>'Europe/Berlin',
'West Asia Standard Time'=>'Asia/Tashkent',
'West Pacific Standard Time'=>'Pacific/Port_Moresby',
'Yakutsk Standard Time'=>'Asia/Yakutsk',
// Microsoft exchange timezones private static function getInstance(): self
// Source: {
// http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx if (null === self::$instance) {
// self::$instance = new self();
// Correct timezones deduced with help from: }
// http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
'Universal Coordinated Time' => 'UTC',
'Casablanca, Monrovia' => 'Africa/Casablanca',
'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London',
'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
'Prague, Central Europe' => 'Europe/Prague',
'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo',
'West Central Africa' => 'Africa/Luanda', // This was a best guess
'Athens, Istanbul, Minsk' => 'Europe/Athens',
'Bucharest' => 'Europe/Bucharest',
'Cairo' => 'Africa/Cairo',
'Harare, Pretoria' => 'Africa/Harare',
'Helsinki, Riga, Tallinn' => 'Europe/Helsinki',
'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem',
'Baghdad' => 'Asia/Baghdad',
'Arab, Kuwait, Riyadh' => 'Asia/Kuwait',
'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
'East Africa, Nairobi' => 'Africa/Nairobi',
'Tehran' => 'Asia/Tehran',
'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess
'Baku, Tbilisi, Yerevan' => 'Asia/Baku',
'Kabul' => 'Asia/Kabul',
'Ekaterinburg' => 'Asia/Yekaterinburg',
'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta',
'Kathmandu, Nepal' => 'Asia/Kathmandu',
'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
'Astana, Dhaka' => 'Asia/Dhaka',
'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
'Rangoon' => 'Asia/Rangoon',
'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
'Krasnoyarsk' => 'Asia/Krasnoyarsk',
'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk',
'Kuala Lumpur, Singapore' => 'Asia/Singapore',
'Perth, Western Australia' => 'Australia/Perth',
'Taipei' => 'Asia/Taipei',
'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
'Seoul, Korea Standard time' => 'Asia/Seoul',
'Yakutsk' => 'Asia/Yakutsk',
'Adelaide, Central Australia' => 'Australia/Adelaide',
'Darwin' => 'Australia/Darwin',
'Brisbane, East Australia' => 'Australia/Brisbane',
'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney',
'Guam, Port Moresby' => 'Pacific/Guam',
'Hobart, Tasmania' => 'Australia/Hobart',
'Vladivostok' => 'Asia/Vladivostok',
'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan',
'Auckland, Wellington' => 'Pacific/Auckland',
'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji',
'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu',
'Azores' => 'Atlantic/Azores',
'Cape Verde Is.' => 'Atlantic/Cape_Verde',
'Mid-Atlantic' => 'America/Noronha',
'Brasilia' => 'America/Sao_Paulo', // Best guess
'Buenos Aires' => 'America/Argentina/Buenos_Aires',
'Greenland' => 'America/Godthab',
'Newfoundland' => 'America/St_Johns',
'Atlantic Time (Canada)' => 'America/Halifax',
'Caracas, La Paz' => 'America/Caracas',
'Santiago' => 'America/Santiago',
'Bogota, Lima, Quito' => 'America/Bogota',
'Eastern Time (US & Canada)' => 'America/New_York',
'Indiana (East)' => 'America/Indiana/Indianapolis',
'Central America' => 'America/Guatemala',
'Central Time (US & Canada)' => 'America/Chicago',
'Mexico City, Tegucigalpa' => 'America/Mexico_City',
'Saskatchewan' => 'America/Edmonton',
'Arizona' => 'America/Phoenix',
'Mountain Time (US & Canada)' => 'America/Denver', // Best guess
'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess
'Alaska' => 'America/Anchorage',
'Hawaii' => 'Pacific/Honolulu',
'Midway Island, Samoa' => 'Pacific/Midway',
'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein',
// The following list are timezone names that could be generated by return self::$instance;
// Lotus / Domino }
'Dateline' => 'Etc/GMT-12',
'Samoa' => 'Pacific/Apia', private function addGuesser(string $key, TimezoneGuesser $guesser): void
'Hawaiian' => 'Pacific/Honolulu', {
'Alaskan' => 'America/Anchorage', $this->timezoneGuessers[$key] = $guesser;
'Pacific' => 'America/Los_Angeles', }
'Pacific Standard Time' => 'America/Los_Angeles',
'Mexico Standard Time 2' => 'America/Chihuahua', private function addFinder(string $key, TimezoneFinder $finder): void
'Mountain' => 'America/Denver', {
'Mountain Standard Time' => 'America/Chihuahua', $this->timezoneFinders[$key] = $finder;
'US Mountain' => 'America/Phoenix', }
'Canada Central' => 'America/Edmonton',
'Central America' => 'America/Guatemala', /**
'Central' => 'America/Chicago', * This method will try to find out the correct timezone for an iCalendar
'Central Standard Time' => 'America/Mexico_City', * date-time value.
'Mexico' => 'America/Mexico_City', *
'Eastern' => 'America/New_York', * You must pass the contents of the TZID parameter, as well as the full
'SA Pacific' => 'America/Bogota', * calendar.
'US Eastern' => 'America/Indiana/Indianapolis', *
'Venezuela' => 'America/Caracas', * If the lookup fails, this method will return the default PHP timezone
'Atlantic' => 'America/Halifax', * (as configured using date_default_timezone_set, or the date.timezone ini
'Central Brazilian' => 'America/Manaus', * setting).
'Pacific SA' => 'America/Santiago', *
'SA Western' => 'America/La_Paz', * Alternatively, if $failIfUncertain is set to true, it will throw an
'Newfoundland' => 'America/St_Johns', * exception if we cannot accurately determine the timezone.
'Argentina' => 'America/Argentina/Buenos_Aires', */
'E. South America' => 'America/Belem', private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): DateTimeZone
'Greenland' => 'America/Godthab', {
'Montevideo' => 'America/Montevideo', foreach ($this->timezoneFinders as $timezoneFinder) {
'SA Eastern' => 'America/Belem', $timezone = $timezoneFinder->find($tzid, $failIfUncertain);
'Mid-Atlantic' => 'Etc/GMT-2', if (!$timezone instanceof DateTimeZone) {
'Azores' => 'Atlantic/Azores', continue;
'Cape Verde' => 'Atlantic/Cape_Verde', }
'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT.
'Morocco' => 'Africa/Casablanca', return $timezone;
'Central Europe' => 'Europe/Prague', }
'Central European' => 'Europe/Sarajevo',
'Romance' => 'Europe/Paris', if ($vcalendar) {
'W. Central Africa' => 'Africa/Lagos', // Best guess // If that didn't work, we will scan VTIMEZONE objects
'W. Europe' => 'Europe/Amsterdam', foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) {
'E. Europe' => 'Europe/Minsk', if ((string) $vtimezone->TZID === $tzid) {
'Egypt' => 'Africa/Cairo', foreach ($this->timezoneGuessers as $timezoneGuesser) {
'FLE' => 'Europe/Helsinki', $timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain);
'GTB' => 'Europe/Athens', if (!$timezone instanceof DateTimeZone) {
'Israel' => 'Asia/Jerusalem', continue;
'Jordan' => 'Asia/Amman', }
'Middle East' => 'Asia/Beirut',
'Namibia' => 'Africa/Windhoek', return $timezone;
'South Africa' => 'Africa/Harare', }
'Arab' => 'Asia/Kuwait', }
'Arabic' => 'Asia/Baghdad', }
'E. Africa' => 'Africa/Nairobi', }
'Georgian' => 'Asia/Tbilisi',
'Russian' => 'Europe/Moscow', if ($failIfUncertain) {
'Iran' => 'Asia/Tehran', throw new InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid);
'Arabian' => 'Asia/Muscat', }
'Armenian' => 'Asia/Yerevan',
'Azerbijan' => 'Asia/Baku', // If we got all the way here, we default to whatever has been set as the PHP default timezone.
'Caucasus' => 'Asia/Yerevan', return new DateTimeZone(date_default_timezone_get());
'Mauritius' => 'Indian/Mauritius', }
'Afghanistan' => 'Asia/Kabul',
'Ekaterinburg' => 'Asia/Yekaterinburg', public static function addTimezoneGuesser(string $key, TimezoneGuesser $guesser): void
'Pakistan' => 'Asia/Karachi', {
'West Asia' => 'Asia/Tashkent', self::getInstance()->addGuesser($key, $guesser);
'India' => 'Asia/Calcutta', }
'Sri Lanka' => 'Asia/Colombo',
'Nepal' => 'Asia/Kathmandu', public static function addTimezoneFinder(string $key, TimezoneFinder $finder): void
'Central Asia' => 'Asia/Dhaka', {
'N. Central Asia' => 'Asia/Almaty', self::getInstance()->addFinder($key, $finder);
'Myanmar' => 'Asia/Rangoon', }
'North Asia' => 'Asia/Krasnoyarsk',
'SE Asia' => 'Asia/Bangkok', /**
'China' => 'Asia/Shanghai', * @param string $tzid
'North Asia East' => 'Asia/Irkutsk', * @param false $failIfUncertain
'Singapore' => 'Asia/Singapore', *
'Taipei' => 'Asia/Taipei', * @return DateTimeZone
'W. Australia' => 'Australia/Perth', */
'Korea' => 'Asia/Seoul', public static function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false)
'Tokyo' => 'Asia/Tokyo', {
'Yakutsk' => 'Asia/Yakutsk', return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain);
'AUS Central' => 'Australia/Darwin', }
'Cen. Australia' => 'Australia/Adelaide',
'AUS Eastern' => 'Australia/Sydney', public static function clean(): void
'E. Australia' => 'Australia/Brisbane', {
'Tasmania' => 'Australia/Hobart', self::$instance = null;
'Vladivostok' => 'Asia/Vladivostok', }
'West Pacific' => 'Pacific/Guam',
'Central Pacific' => 'Asia/Magadan', // Keeping things for backwards compatibility
'Fiji' => 'Pacific/Fiji', /**
'New Zealand' => 'Pacific/Auckland', * @var array|null
'Tonga' => 'Pacific/Tongatapu', *
); * @deprecated
*/
public static $map = null;
/** /**
* List of microsoft exchange timezone ids. * List of microsoft exchange timezone ids.
* *
* Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
*
* @deprecated
*/ */
public static $microsoftExchangeMap = array( public static $microsoftExchangeMap = [
0 => 'UTC', 0 => 'UTC',
31 => 'Africa/Casablanca', 31 => 'Africa/Casablanca',
// Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
// I'm not even kidding.. We handle this special case in the // I'm not even kidding.. We handle this special case in the
// getTimeZone method. // getTimeZone method.
@ -385,98 +230,43 @@ class TimeZoneUtil {
15 => 'Pacific/Honolulu', 15 => 'Pacific/Honolulu',
16 => 'Pacific/Midway', 16 => 'Pacific/Midway',
39 => 'Pacific/Kwajalein', 39 => 'Pacific/Kwajalein',
); ];
/** /**
* This method will try to find out the correct timezone for an iCalendar * This method will load in all the tz mapping information, if it's not yet
* date-time value. * done.
* *
* You must pass the contents of the TZID parameter, as well as the full * @deprecated
* calendar.
*
* If the lookup fails, this method will return the default PHP timezone
* (as configured using date_default_timezone_set, or the date.timezone ini
* setting).
*
* Alternatively, if $failIfUncertain is set to true, it will throw an
* exception if we cannot accurately determine the timezone.
*
* @param string $tzid
* @param Sabre\VObject\Component $vcalendar
* @return DateTimeZone
*/ */
static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) { public static function loadTzMaps()
{
// First we will just see if the tzid is a support timezone identifier. if (!is_null(self::$map)) {
try { return;
return new \DateTimeZone($tzid);
} catch (\Exception $e) {
} }
// Next, we check if the tzid is somewhere in our tzid map. self::$map = array_merge(
if (isset(self::$map[$tzid])) { include __DIR__.'/timezonedata/windowszones.php',
return new \DateTimeZone(self::$map[$tzid]); include __DIR__.'/timezonedata/lotuszones.php',
include __DIR__.'/timezonedata/exchangezones.php',
include __DIR__.'/timezonedata/php-workaround.php'
);
} }
// Maybe the author was hyper-lazy and just included an offset. We /**
// support it, but we aren't happy about it. * This method returns an array of timezone identifiers, that are supported
if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) { * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0')); *
} * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
* - It's not supported by some PHP versions as well as HHVM.
if ($vcalendar) { * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
* (See timezonedata/php-bc.php and timezonedata php-workaround.php)
// If that didn't work, we will scan VTIMEZONE objects *
foreach($vcalendar->select('VTIMEZONE') as $vtimezone) { * @return array
*
if ((string)$vtimezone->TZID === $tzid) { * @deprecated
*/
// Some clients add 'X-LIC-LOCATION' with the olson name. public static function getIdentifiersBC()
if (isset($vtimezone->{'X-LIC-LOCATION'})) { {
return include __DIR__.'/timezonedata/php-bc.php';
$lic = (string)$vtimezone->{'X-LIC-LOCATION'};
// Libical generators may specify strings like
// "SystemV/EST5EDT". For those we must remove the
// SystemV part.
if (substr($lic,0,8)==='SystemV/') {
$lic = substr($lic,8);
}
try {
return new \DateTimeZone($lic);
} catch (\Exception $e) {
}
}
// Microsoft may add a magic number, which we also have an
// answer for.
if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
$cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
// 2 can mean both Europe/Lisbon and Europe/Sarajevo.
if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) {
return new \DateTimeZone('Europe/Sarajevo');
}
if (isset(self::$microsoftExchangeMap[$cdoId])) {
return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
} }
} }
}
}
}
if ($failIfUncertain) {
throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid);
}
// If we got all the way here, we default to UTC.
return new \DateTimeZone(date_default_timezone_get());
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Sabre\VObject\TimezoneGuesser;
use DateTimeZone;
/**
* Some clients add 'X-LIC-LOCATION' with the olson name.
*/
class FindFromOffset implements TimezoneFinder
{
public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
{
// Maybe the author was hyper-lazy and just included an offset. We
// support it, but we aren't happy about it.
if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
// Note that the path in the source will never be taken from PHP 5.5.10
// onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it
// already gets returned early in this function. Once we drop support
// for versions under PHP 5.5.10, this bit can be taken out of the
// source.
// @codeCoverageIgnoreStart
return new DateTimeZone('Etc/GMT'.$matches[1].ltrim(substr($matches[2], 0, 2), '0'));
// @codeCoverageIgnoreEnd
}
return null;
}
}

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Sabre\VObject\TimezoneGuesser;
use DateTimeZone;
use Exception;
/**
* Some clients add 'X-LIC-LOCATION' with the olson name.
*/
class FindFromTimezoneIdentifier implements TimezoneFinder
{
public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
{
// First we will just see if the tzid is a support timezone identifier.
//
// The only exception is if the timezone starts with (. This is to
// handle cases where certain microsoft products generate timezone
// identifiers that for instance look like:
//
// (GMT+01.00) Sarajevo/Warsaw/Zagreb
//
// Since PHP 5.5.10, the first bit will be used as the timezone and
// this method will return just GMT+01:00. This is wrong, because it
// doesn't take DST into account
if (!isset($tzid[0])) {
return null;
}
if ('(' === $tzid[0]) {
return null;
}
// PHP has a bug that logs PHP warnings even it shouldn't:
// https://bugs.php.net/bug.php?id=67881
//
// That's why we're checking if we'll be able to successfully instantiate
// \DateTimeZone() before doing so. Otherwise we could simply instantiate
// and catch the exception.
$tzIdentifiers = DateTimeZone::listIdentifiers();
try {
if (
(in_array($tzid, $tzIdentifiers)) ||
(preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) ||
(in_array($tzid, $this->getIdentifiersBC()))
) {
return new DateTimeZone($tzid);
}
} catch (Exception $e) {
}
return null;
}
/**
* This method returns an array of timezone identifiers, that are supported
* by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
*
* We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
* - It's not supported by some PHP versions as well as HHVM.
* - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
* (See timezonedata/php-bc.php and timezonedata php-workaround.php)
*
* @return array
*/
private function getIdentifiersBC()
{
return include __DIR__.'/../timezonedata/php-bc.php';
}
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Sabre\VObject\TimezoneGuesser;
use DateTimeZone;
/**
* Some clients add 'X-LIC-LOCATION' with the olson name.
*/
class FindFromTimezoneMap implements TimezoneFinder
{
private $map = [];
private $patterns = [
'/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/',
'/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/',
];
public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
{
// Next, we check if the tzid is somewhere in our tzid map.
if ($this->hasTzInMap($tzid)) {
return new DateTimeZone($this->getTzFromMap($tzid));
}
// Some Microsoft products prefix the offset first, so let's strip that off
// and see if it is our tzid map. We don't want to check for this first just
// in case there are overrides in our tzid map.
foreach ($this->patterns as $pattern) {
if (!preg_match($pattern, $tzid, $matches)) {
continue;
}
$tzidAlternate = $matches[3];
if ($this->hasTzInMap($tzidAlternate)) {
return new DateTimeZone($this->getTzFromMap($tzidAlternate));
}
}
return null;
}
/**
* This method returns an array of timezone identifiers, that are supported
* by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
*
* We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
* - It's not supported by some PHP versions as well as HHVM.
* - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
* (See timezonedata/php-bc.php and timezonedata php-workaround.php)
*
* @return array
*/
private function getTzMaps()
{
if ([] === $this->map) {
$this->map = array_merge(
include __DIR__.'/../timezonedata/windowszones.php',
include __DIR__.'/../timezonedata/lotuszones.php',
include __DIR__.'/../timezonedata/exchangezones.php',
include __DIR__.'/../timezonedata/php-workaround.php'
);
}
return $this->map;
}
private function getTzFromMap(string $tzid): string
{
return $this->getTzMaps()[$tzid];
}
private function hasTzInMap(string $tzid): bool
{
return isset($this->getTzMaps()[$tzid]);
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Sabre\VObject\TimezoneGuesser;
use DateTimeZone;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\TimeZoneUtil;
/**
* Some clients add 'X-LIC-LOCATION' with the olson name.
*/
class GuessFromLicEntry implements TimezoneGuesser
{
public function guess(VTimeZone $vtimezone, bool $failIfUncertain = false): ?DateTimeZone
{
if (!isset($vtimezone->{'X-LIC-LOCATION'})) {
return null;
}
$lic = (string) $vtimezone->{'X-LIC-LOCATION'};
// Libical generators may specify strings like
// "SystemV/EST5EDT". For those we must remove the
// SystemV part.
if ('SystemV/' === substr($lic, 0, 8)) {
$lic = substr($lic, 8);
}
return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain);
}
}

View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Sabre\VObject\TimezoneGuesser;
use DateTimeZone;
use Sabre\VObject\Component\VTimeZone;
class GuessFromMsTzId implements TimezoneGuesser
{
/**
* List of microsoft exchange timezone ids.
*
* Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
*/
public static $microsoftExchangeMap = [
0 => 'UTC',
31 => 'Africa/Casablanca',
// Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
// I'm not even kidding.. We handle this special case in the
// getTimeZone method.
2 => 'Europe/Lisbon',
1 => 'Europe/London',
4 => 'Europe/Berlin',
6 => 'Europe/Prague',
3 => 'Europe/Paris',
69 => 'Africa/Luanda', // This was a best guess
7 => 'Europe/Athens',
5 => 'Europe/Bucharest',
49 => 'Africa/Cairo',
50 => 'Africa/Harare',
59 => 'Europe/Helsinki',
27 => 'Asia/Jerusalem',
26 => 'Asia/Baghdad',
74 => 'Asia/Kuwait',
51 => 'Europe/Moscow',
56 => 'Africa/Nairobi',
25 => 'Asia/Tehran',
24 => 'Asia/Muscat', // Best guess
54 => 'Asia/Baku',
48 => 'Asia/Kabul',
58 => 'Asia/Yekaterinburg',
47 => 'Asia/Karachi',
23 => 'Asia/Calcutta',
62 => 'Asia/Kathmandu',
46 => 'Asia/Almaty',
71 => 'Asia/Dhaka',
66 => 'Asia/Colombo',
61 => 'Asia/Rangoon',
22 => 'Asia/Bangkok',
64 => 'Asia/Krasnoyarsk',
45 => 'Asia/Shanghai',
63 => 'Asia/Irkutsk',
21 => 'Asia/Singapore',
73 => 'Australia/Perth',
75 => 'Asia/Taipei',
20 => 'Asia/Tokyo',
72 => 'Asia/Seoul',
70 => 'Asia/Yakutsk',
19 => 'Australia/Adelaide',
44 => 'Australia/Darwin',
18 => 'Australia/Brisbane',
76 => 'Australia/Sydney',
43 => 'Pacific/Guam',
42 => 'Australia/Hobart',
68 => 'Asia/Vladivostok',
41 => 'Asia/Magadan',
17 => 'Pacific/Auckland',
40 => 'Pacific/Fiji',
67 => 'Pacific/Tongatapu',
29 => 'Atlantic/Azores',
53 => 'Atlantic/Cape_Verde',
30 => 'America/Noronha',
8 => 'America/Sao_Paulo', // Best guess
32 => 'America/Argentina/Buenos_Aires',
60 => 'America/Godthab',
28 => 'America/St_Johns',
9 => 'America/Halifax',
33 => 'America/Caracas',
65 => 'America/Santiago',
35 => 'America/Bogota',
10 => 'America/New_York',
34 => 'America/Indiana/Indianapolis',
55 => 'America/Guatemala',
11 => 'America/Chicago',
37 => 'America/Mexico_City',
36 => 'America/Edmonton',
38 => 'America/Phoenix',
12 => 'America/Denver', // Best guess
13 => 'America/Los_Angeles', // Best guess
14 => 'America/Anchorage',
15 => 'Pacific/Honolulu',
16 => 'Pacific/Midway',
39 => 'Pacific/Kwajalein',
];
public function guess(VTimeZone $vtimezone, bool $throwIfUnsure = false): ?DateTimeZone
{
// Microsoft may add a magic number, which we also have an
// answer for.
if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
return null;
}
$cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
// 2 can mean both Europe/Lisbon and Europe/Sarajevo.
if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) {
return new DateTimeZone('Europe/Sarajevo');
}
if (isset(self::$microsoftExchangeMap[$cdoId])) {
return new DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
}
return null;
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Sabre\VObject\TimezoneGuesser;
use DateTimeZone;
interface TimezoneFinder
{
public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone;
}

View file

@ -0,0 +1,11 @@
<?php
namespace Sabre\VObject\TimezoneGuesser;
use DateTimeZone;
use Sabre\VObject\Component\VTimeZone;
interface TimezoneGuesser
{
public function guess(VTimeZone $vtimezone, bool $failIfUncertain = false): ?DateTimeZone;
}

View file

@ -0,0 +1,66 @@
<?php
namespace Sabre\VObject;
/**
* UUID Utility.
*
* This class has static methods to generate and validate UUID's.
* UUIDs are used a decent amount within various *DAV standards, so it made
* sense to include it.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class UUIDUtil
{
/**
* Returns a pseudo-random v4 UUID.
*
* This function is based on a comment by Andrew Moore on php.net
*
* @see http://www.php.net/manual/en/function.uniqid.php#94959
*
* @return string
*/
public static function getUUID()
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
// 16 bits for "time_mid"
mt_rand(0, 0xffff),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
mt_rand(0, 0x0fff) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
mt_rand(0, 0x3fff) | 0x8000,
// 48 bits for "node"
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
/**
* Checks if a string is a valid UUID.
*
* @param string $uuid
*
* @return bool
*/
public static function validateUUID($uuid)
{
return 0 !== preg_match(
'/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i',
$uuid
);
}
}

View file

@ -5,12 +5,12 @@ namespace Sabre\VObject;
/** /**
* This utility converts vcards from one version to another. * This utility converts vcards from one version to another.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class VCardConverter { class VCardConverter
{
/** /**
* Converts a vCard object to a new version. * Converts a vCard object to a new version.
* *
@ -26,57 +26,51 @@ class VCardConverter {
* *
* If input and output version are identical, a clone is returned. * If input and output version are identical, a clone is returned.
* *
* @param Component\VCard $input
* @param int $targetVersion * @param int $targetVersion
*/ */
public function convert(Component\VCard $input, $targetVersion) { public function convert(Component\VCard $input, $targetVersion)
{
$inputVersion = $input->getDocumentType(); $inputVersion = $input->getDocumentType();
if ($inputVersion === $targetVersion) { if ($inputVersion === $targetVersion) {
return clone $input; return clone $input;
} }
if (!in_array($inputVersion, array(Document::VCARD21, Document::VCARD30, Document::VCARD40))) { if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) {
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, array(Document::VCARD30, Document::VCARD40))) { if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) {
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');
} }
$newVersion = $targetVersion===Document::VCARD40?'4.0':'3.0'; $newVersion = Document::VCARD40 === $targetVersion ? '4.0' : '3.0';
$output = new Component\VCard(array( $output = new Component\VCard([
'VERSION' => $newVersion, 'VERSION' => $newVersion,
)); ]);
foreach($input->children as $property) { // We might have generated a default UID. Remove it!
unset($output->UID);
foreach ($input->children() as $property) {
$this->convertProperty($input, $output, $property, $targetVersion); $this->convertProperty($input, $output, $property, $targetVersion);
} }
return $output; return $output;
} }
/** /**
* Handles conversion of a single property. * Handles conversion of a single property.
* *
* @param Component\VCard $input
* @param Component\VCard $output
* @param Property $property
* @param int $targetVersion * @param int $targetVersion
* @return void
*/ */
protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) { protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion)
{
// Skipping these, those are automatically added. // Skipping these, those are automatically added.
if (in_array($property->name, array('VERSION', 'PRODID'))) { if (in_array($property->name, ['VERSION', 'PRODID'])) {
return; return;
} }
$parameters = $property->parameters(); $parameters = $property->parameters();
$valueType = null; $valueType = null;
if (isset($parameters['VALUE'])) { if (isset($parameters['VALUE'])) {
$valueType = $parameters['VALUE']->getValue(); $valueType = $parameters['VALUE']->getValue();
@ -85,24 +79,60 @@ class VCardConverter {
if (!$valueType) { if (!$valueType) {
$valueType = $property->getValueType(); $valueType = $property->getValueType();
} }
if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) {
$valueType = null;
}
$newProperty = $output->createProperty(
$property->name,
$property->getParts(),
[], // parameters will get added a bit later.
$valueType
);
$newProperty = null; if (Document::VCARD30 === $targetVersion) {
if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) {
$newProperty = $this->convertUriToBinary($output, $newProperty);
} elseif ($property instanceof Property\VCard\DateAndOrTime) {
// In vCard 4, the birth year may be optional. This is not the
// case for vCard 3. Apple has a workaround for this that
// allows applications that support Apple's extension still
// omit birthyears in vCard 3, but applications that do not
// support this, will just use a random birthyear. We're
// choosing 1604 for the birthyear, because that's what apple
// uses.
$parts = DateTimeParser::parseVCardDateTime($property->getValue());
if (is_null($parts['year'])) {
$newValue = '1604-'.$parts['month'].'-'.$parts['date'];
$newProperty->setValue($newValue);
$newProperty['X-APPLE-OMIT-YEAR'] = '1604';
}
if ($targetVersion===Document::VCARD30) { if ('ANNIVERSARY' == $newProperty->name) {
// Microsoft non-standard anniversary
if ($property instanceof Property\Uri && in_array($property->name, array('PHOTO','LOGO','SOUND'))) { $newProperty->name = 'X-ANNIVERSARY';
$newProperty = $this->convertUriToBinary($output, $property, $parameters);
} elseif ($property->name === 'KIND') {
// We also need to add a new apple property for the same
// purpose. This apple property needs a 'label' in the same
// group, so we first need to find a groupname that doesn't
// exist yet.
$x = 1;
while ($output->select('ITEM'.$x.'.')) {
++$x;
}
$output->add('ITEM'.$x.'.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']);
$output->add('ITEM'.$x.'.X-ABLABEL', '_$!<Anniversary>!$_');
}
} elseif ('KIND' === $property->name) {
switch (strtolower($property->getValue())) { switch (strtolower($property->getValue())) {
case 'org': case 'org':
// OS X addressbook property. // vCard 3.0 does not have an equivalent to KIND:ORG,
// but apple has an extension that means the same
// thing.
$newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY'); $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY');
break; break;
case 'individual': case 'individual':
// Individual is implied, so we can just skip it. // Individual is implicit, so we skip it.
return; return;
case 'group': case 'group':
@ -110,55 +140,85 @@ class VCardConverter {
$newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP'); $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP');
break; break;
} }
} }
} elseif (Document::VCARD40 === $targetVersion) {
} elseif ($targetVersion===Document::VCARD40) {
// These properties were removed in vCard 4.0 // These properties were removed in vCard 4.0
if (in_array($property->name, array('NAME', 'MAILER', 'LABEL', 'CLASS'))) { if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) {
return; return;
} }
if ($property instanceOf Property\Binary) { if ($property instanceof Property\Binary) {
$newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters);
} elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) {
// If a property such as BDAY contained 'X-APPLE-OMIT-YEAR',
// then we're stripping the year from the vcard 4 value.
$parts = DateTimeParser::parseVCardDateTime($property->getValue());
if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) {
$newValue = '--'.$parts['month'].'-'.$parts['date'];
$newProperty->setValue($newValue);
}
$newProperty = $this->convertBinaryToUri($output, $property, $parameters); // Regardless if the year matched or not, we do need to strip
// X-APPLE-OMIT-YEAR.
} else { unset($parameters['X-APPLE-OMIT-YEAR']);
}
switch ($property->name) { switch ($property->name) {
case 'X-ABSHOWAS': case 'X-ABSHOWAS':
if (strtoupper($property->getValue()) === 'COMPANY') { if ('COMPANY' === strtoupper($property->getValue())) {
$newProperty = $output->createProperty('KIND','org'); $newProperty = $output->createProperty('KIND', 'ORG');
} }
break; break;
case 'X-ADDRESSBOOKSERVER-KIND': case 'X-ADDRESSBOOKSERVER-KIND':
if (strtoupper($property->getValue()) === 'GROUP') { if ('GROUP' === strtoupper($property->getValue())) {
$newProperty = $output->createProperty('KIND','group'); $newProperty = $output->createProperty('KIND', 'GROUP');
} }
break; break;
case 'X-ANNIVERSARY':
$newProperty->name = 'ANNIVERSARY';
// If we already have an anniversary property with the same
// value, ignore.
foreach ($output->select('ANNIVERSARY') as $anniversary) {
if ($anniversary->getValue() === $newProperty->getValue()) {
return;
}
}
break;
case 'X-ABDATE':
// Find out what the label was, if it exists.
if (!$property->group) {
break;
}
$label = $input->{$property->group.'.X-ABLABEL'};
// We only support converting anniversaries.
if (!$label || '_$!<Anniversary>!$_' !== $label->getValue()) {
break;
} }
// If we already have an anniversary property with the same
// value, ignore.
foreach ($output->select('ANNIVERSARY') as $anniversary) {
if ($anniversary->getValue() === $newProperty->getValue()) {
return;
} }
} }
$newProperty->name = 'ANNIVERSARY';
break;
if (is_null($newProperty)) { // Apple's per-property label system.
case 'X-ABLABEL':
$newProperty = $output->createProperty( if ('_$!<Anniversary>!$_' === $newProperty->getValue()) {
$property->name, // We can safely remove these, as they are converted to
$property->getParts(), // ANNIVERSARY properties.
array(), // no parameters yet return;
$valueType }
); break;
}
} }
// set property group // set property group
$newProperty->group = $property->group; $newProperty->group = $property->group;
if ($targetVersion===Document::VCARD40) { if (Document::VCARD40 === $targetVersion) {
$this->convertParameters40($newProperty, $parameters); $this->convertParameters40($newProperty, $parameters);
} else { } else {
$this->convertParameters30($newProperty, $parameters); $this->convertParameters30($newProperty, $parameters);
@ -166,7 +226,7 @@ class VCardConverter {
// Lastly, we need to see if there's a need for a VALUE parameter. // Lastly, we need to see if there's a need for a VALUE parameter.
// //
// We can do that by instantating a empty property with that name, and // We can do that by instantiating a empty property with that name, and
// seeing if the default valueType is identical to the current one. // seeing if the default valueType is identical to the current one.
$tempProperty = $output->createProperty($newProperty->name); $tempProperty = $output->createProperty($newProperty->name);
if ($tempProperty->getValueType() !== $newProperty->getValueType()) { if ($tempProperty->getValueType() !== $newProperty->getValueType()) {
@ -174,8 +234,6 @@ class VCardConverter {
} }
$output->add($newProperty); $output->add($newProperty);
} }
/** /**
@ -183,18 +241,19 @@ class VCardConverter {
* *
* vCard 4.0 no longer supports BINARY properties. * vCard 4.0 no longer supports BINARY properties.
* *
* @param Component\VCard $output * @param Property\Uri $property the input property
* @param Property\Uri $property The input property. * @param $parameters list of parameters that will eventually be added to
* @param $parameters List of parameters that will eventually be added to * the new property
* the new property. *
* @return Property\Uri * @return Property\Uri
*/ */
protected function convertBinaryToUri(Component\VCard $output, Property\Binary $property, array &$parameters) { protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters)
{
$value = $newProperty->getValue();
$newProperty = $output->createProperty( $newProperty = $output->createProperty(
$property->name, $newProperty->name,
null, // no value null, // no value
array(), // no parameters yet [], // no parameters yet
'URI' // Forcing the BINARY type 'URI' // Forcing the BINARY type
); );
@ -202,12 +261,11 @@ class VCardConverter {
// See if we can find a better mimetype. // See if we can find a better mimetype.
if (isset($parameters['TYPE'])) { if (isset($parameters['TYPE'])) {
$newTypes = [];
$newTypes = array();
foreach ($parameters['TYPE']->getParts() as $typePart) { foreach ($parameters['TYPE']->getParts() as $typePart) {
if (in_array( if (in_array(
strtoupper($typePart), strtoupper($typePart),
array('JPEG','PNG','GIF') ['JPEG', 'PNG', 'GIF']
)) { )) {
$mimeType = 'image/'.strtolower($typePart); $mimeType = 'image/'.strtolower($typePart);
} else { } else {
@ -222,13 +280,11 @@ class VCardConverter {
} else { } else {
unset($parameters['TYPE']); unset($parameters['TYPE']);
} }
} }
$newProperty->setValue('data:' . $mimeType . ';base64,' . base64_encode($property->getValue())); $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value));
return $newProperty; return $newProperty;
} }
/** /**
@ -238,25 +294,23 @@ class VCardConverter {
* be valid in vCard 3.0 as well, we should convert those to BINARY if * be valid in vCard 3.0 as well, we should convert those to BINARY if
* possible, to improve compatibility. * possible, to improve compatibility.
* *
* @param Component\VCard $output * @param Property\Uri $property the input property
* @param Property\Uri $property The input property. *
* @param $parameters List of parameters that will eventually be added to
* the new property.
* @return Property\Binary|null * @return Property\Binary|null
*/ */
protected function convertUriToBinary(Component\VCard $output, Property\Uri $property, array &$parameters) { protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty)
{
$value = $property->getValue(); $value = $newProperty->getValue();
// Only converting data: uris // Only converting data: uris
if (substr($value, 0, 5)!=='data:') { if ('data:' !== substr($value, 0, 5)) {
return; return $newProperty;
} }
$newProperty = $output->createProperty( $newProperty = $output->createProperty(
$property->name, $newProperty->name,
null, // no value null, // no value
array(), // no parameters yet [], // no parameters yet
'BINARY' 'BINARY'
); );
@ -271,7 +325,6 @@ class VCardConverter {
$newProperty['ENCODING'] = 'b'; $newProperty['ENCODING'] = 'b';
switch ($mimeType) { switch ($mimeType) {
case 'image/jpeg': case 'image/jpeg':
$newProperty['TYPE'] = 'JPEG'; $newProperty['TYPE'] = 'JPEG';
break; break;
@ -281,42 +334,33 @@ class VCardConverter {
case 'image/gif': case 'image/gif':
$newProperty['TYPE'] = 'GIF'; $newProperty['TYPE'] = 'GIF';
break; break;
} }
return $newProperty; return $newProperty;
} }
/** /**
* Adds parameters to a new property for vCard 4.0 * Adds parameters to a new property for vCard 4.0.
*
* @param Property $newProperty
* @param array $parameters
* @return void
*/ */
protected function convertParameters40(Property $newProperty, array $parameters) { protected function convertParameters40(Property $newProperty, array $parameters)
{
// Adding all parameters. // Adding all parameters.
foreach ($parameters as $param) { foreach ($parameters as $param) {
// vCard 2.1 allowed parameters with no name // vCard 2.1 allowed parameters with no name
if ($param->noName) $param->noName = false; if ($param->noName) {
$param->noName = false;
}
switch ($param->name) { switch ($param->name) {
// We need to see if there's any TYPE=PREF, because in vCard 4 // We need to see if there's any TYPE=PREF, because in vCard 4
// 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 (strtoupper($paramPart)==='PREF') {
$newProperty->add('PREF', '1'); $newProperty->add('PREF', '1');
} else { } else {
$newProperty->add($param->name, $paramPart); $newProperty->add($param->name, $paramPart);
} }
} }
break; break;
// These no longer exist in vCard 4 // These no longer exist in vCard 4
@ -327,34 +371,27 @@ class VCardConverter {
default: default:
$newProperty->add($param->name, $param->getParts()); $newProperty->add($param->name, $param->getParts());
break; break;
} }
} }
} }
/** /**
* Adds parameters to a new property for vCard 3.0 * Adds parameters to a new property for vCard 3.0.
*
* @param Property $newProperty
* @param array $parameters
* @return void
*/ */
protected function convertParameters30(Property $newProperty, array $parameters) { protected function convertParameters30(Property $newProperty, array $parameters)
{
// Adding all parameters. // Adding all parameters.
foreach ($parameters as $param) { foreach ($parameters as $param) {
// vCard 2.1 allowed parameters with no name // vCard 2.1 allowed parameters with no name
if ($param->noName) $param->noName = false; if ($param->noName) {
$param->noName = false;
}
switch ($param->name) { switch ($param->name) {
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 (strtoupper($param->getValue())!=='QUOTED-PRINTABLE') { if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) {
$newProperty->add($param->name, $param->getParts()); $newProperty->add($param->name, $param->getParts());
} }
break; break;
@ -365,7 +402,7 @@ class VCardConverter {
* Any other PREF numbers we'll drop. * Any other PREF numbers we'll drop.
*/ */
case 'PREF': case 'PREF':
if ($param->getValue()=='1') { if ('1' == $param->getValue()) {
$newProperty->add('TYPE', 'PREF'); $newProperty->add('TYPE', 'PREF');
} }
break; break;
@ -373,10 +410,7 @@ class VCardConverter {
default: default:
$newProperty->add($param->name, $param->getParts()); $newProperty->add($param->name, $param->getParts());
break; break;
}
} }
}
} }
} }

View file

@ -3,17 +3,16 @@
namespace Sabre\VObject; namespace Sabre\VObject;
/** /**
* This class contains the version number for the VObject package * This class contains the version number for the VObject package.
* *
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/). * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Version { class Version
{
/** /**
* Full version number * Full version number.
*/ */
const VERSION = '3.1.3'; const VERSION = '4.4.1';
} }

View file

@ -0,0 +1,68 @@
<?php
namespace Sabre\VObject;
use Sabre\Xml;
/**
* iCalendar/vCard/jCal/jCard/xCal/xCard writer object.
*
* This object provides a few (static) convenience methods to quickly access
* the serializers.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Ivan Enderlin
* @license http://sabre.io/license/ Modified BSD License
*/
class Writer
{
/**
* Serializes a vCard or iCalendar object.
*
* @return string
*/
public static function write(Component $component)
{
return $component->serialize();
}
/**
* Serializes a jCal or jCard object.
*
* @param int $options
*
* @return string
*/
public static function writeJson(Component $component, $options = 0)
{
return json_encode($component, $options);
}
/**
* Serializes a xCal or xCard object.
*
* @return string
*/
public static function writeXml(Component $component)
{
$writer = new Xml\Writer();
$writer->openMemory();
$writer->setIndent(true);
$writer->startDocument('1.0', 'utf-8');
if ($component instanceof Component\VCalendar) {
$writer->startElement('icalendar');
$writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE);
} else {
$writer->startElement('vcards');
$writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE);
}
$component->xmlSerialize($writer);
$writer->endElement();
return $writer->outputMemory();
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Microsoft exchange timezones
* Source:
* http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx.
*
* Correct timezones deduced with help from:
* http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
return [
'Universal Coordinated Time' => 'UTC',
'Casablanca, Monrovia' => 'Africa/Casablanca',
'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London',
'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
'Prague, Central Europe' => 'Europe/Prague',
'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo',
'West Central Africa' => 'Africa/Luanda', // This was a best guess
'Athens, Istanbul, Minsk' => 'Europe/Athens',
'Bucharest' => 'Europe/Bucharest',
'Cairo' => 'Africa/Cairo',
'Harare, Pretoria' => 'Africa/Harare',
'Helsinki, Riga, Tallinn' => 'Europe/Helsinki',
'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem',
'Baghdad' => 'Asia/Baghdad',
'Arab, Kuwait, Riyadh' => 'Asia/Kuwait',
'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
'East Africa, Nairobi' => 'Africa/Nairobi',
'Tehran' => 'Asia/Tehran',
'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess
'Baku, Tbilisi, Yerevan' => 'Asia/Baku',
'Kabul' => 'Asia/Kabul',
'Ekaterinburg' => 'Asia/Yekaterinburg',
'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta',
'Kathmandu, Nepal' => 'Asia/Kathmandu',
'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
'Astana, Dhaka' => 'Asia/Dhaka',
'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
'Rangoon' => 'Asia/Rangoon',
'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
'Krasnoyarsk' => 'Asia/Krasnoyarsk',
'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk',
'Kuala Lumpur, Singapore' => 'Asia/Singapore',
'Perth, Western Australia' => 'Australia/Perth',
'Taipei' => 'Asia/Taipei',
'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
'Seoul, Korea Standard time' => 'Asia/Seoul',
'Yakutsk' => 'Asia/Yakutsk',
'Adelaide, Central Australia' => 'Australia/Adelaide',
'Darwin' => 'Australia/Darwin',
'Brisbane, East Australia' => 'Australia/Brisbane',
'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney',
'Guam, Port Moresby' => 'Pacific/Guam',
'Hobart, Tasmania' => 'Australia/Hobart',
'Vladivostok' => 'Asia/Vladivostok',
'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan',
'Auckland, Wellington' => 'Pacific/Auckland',
'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji',
'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu',
'Azores' => 'Atlantic/Azores',
'Cape Verde Is.' => 'Atlantic/Cape_Verde',
'Mid-Atlantic' => 'America/Noronha',
'Brasilia' => 'America/Sao_Paulo', // Best guess
'Buenos Aires' => 'America/Argentina/Buenos_Aires',
'Greenland' => 'America/Godthab',
'Newfoundland' => 'America/St_Johns',
'Atlantic Time (Canada)' => 'America/Halifax',
'Caracas, La Paz' => 'America/Caracas',
'Santiago' => 'America/Santiago',
'Bogota, Lima, Quito' => 'America/Bogota',
'Eastern Time (US & Canada)' => 'America/New_York',
'Indiana (East)' => 'America/Indiana/Indianapolis',
'Central America' => 'America/Guatemala',
'Central Time (US & Canada)' => 'America/Chicago',
'Mexico City, Tegucigalpa' => 'America/Mexico_City',
'Saskatchewan' => 'America/Edmonton',
'Arizona' => 'America/Phoenix',
'Mountain Time (US & Canada)' => 'America/Denver', // Best guess
'Pacific Time (US & Canada)' => 'America/Los_Angeles', // Best guess
'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess
'Alaska' => 'America/Anchorage',
'Hawaii' => 'Pacific/Honolulu',
'Midway Island, Samoa' => 'Pacific/Midway',
'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein',
];

View file

@ -0,0 +1,101 @@
<?php
/**
* The following list are timezone names that could be generated by
* Lotus / Domino.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
return [
'Dateline' => 'Etc/GMT-12',
'Samoa' => 'Pacific/Apia',
'Hawaiian' => 'Pacific/Honolulu',
'Alaskan' => 'America/Anchorage',
'Pacific' => 'America/Los_Angeles',
'Pacific Standard Time' => 'America/Los_Angeles',
'Mexico Standard Time 2' => 'America/Chihuahua',
'Mountain' => 'America/Denver',
// 'Mountain Standard Time' => 'America/Chihuahua', // conflict with windows timezones.
'US Mountain' => 'America/Phoenix',
'Canada Central' => 'America/Edmonton',
'Central America' => 'America/Guatemala',
'Central' => 'America/Chicago',
// 'Central Standard Time' => 'America/Mexico_City', // conflict with windows timezones.
'Mexico' => 'America/Mexico_City',
'Eastern' => 'America/New_York',
'SA Pacific' => 'America/Bogota',
'US Eastern' => 'America/Indiana/Indianapolis',
'Venezuela' => 'America/Caracas',
'Atlantic' => 'America/Halifax',
'Central Brazilian' => 'America/Manaus',
'Pacific SA' => 'America/Santiago',
'SA Western' => 'America/La_Paz',
'Newfoundland' => 'America/St_Johns',
'Argentina' => 'America/Argentina/Buenos_Aires',
'E. South America' => 'America/Belem',
'Greenland' => 'America/Godthab',
'Montevideo' => 'America/Montevideo',
'SA Eastern' => 'America/Belem',
// 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones.
'Azores' => 'Atlantic/Azores',
'Cape Verde' => 'Atlantic/Cape_Verde',
'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT.
'Morocco' => 'Africa/Casablanca',
'Central Europe' => 'Europe/Prague',
'Central European' => 'Europe/Sarajevo',
'Romance' => 'Europe/Paris',
'W. Central Africa' => 'Africa/Lagos', // Best guess
'W. Europe' => 'Europe/Amsterdam',
'E. Europe' => 'Europe/Minsk',
'Egypt' => 'Africa/Cairo',
'FLE' => 'Europe/Helsinki',
'GTB' => 'Europe/Athens',
'Israel' => 'Asia/Jerusalem',
'Jordan' => 'Asia/Amman',
'Middle East' => 'Asia/Beirut',
'Namibia' => 'Africa/Windhoek',
'South Africa' => 'Africa/Harare',
'Arab' => 'Asia/Kuwait',
'Arabic' => 'Asia/Baghdad',
'E. Africa' => 'Africa/Nairobi',
'Georgian' => 'Asia/Tbilisi',
'Russian' => 'Europe/Moscow',
'Iran' => 'Asia/Tehran',
'Arabian' => 'Asia/Muscat',
'Armenian' => 'Asia/Yerevan',
'Azerbijan' => 'Asia/Baku',
'Caucasus' => 'Asia/Yerevan',
'Mauritius' => 'Indian/Mauritius',
'Afghanistan' => 'Asia/Kabul',
'Ekaterinburg' => 'Asia/Yekaterinburg',
'Pakistan' => 'Asia/Karachi',
'West Asia' => 'Asia/Tashkent',
'India' => 'Asia/Calcutta',
'Sri Lanka' => 'Asia/Colombo',
'Nepal' => 'Asia/Kathmandu',
'Central Asia' => 'Asia/Dhaka',
'N. Central Asia' => 'Asia/Almaty',
'Myanmar' => 'Asia/Rangoon',
'North Asia' => 'Asia/Krasnoyarsk',
'SE Asia' => 'Asia/Bangkok',
'China' => 'Asia/Shanghai',
'North Asia East' => 'Asia/Irkutsk',
'Singapore' => 'Asia/Singapore',
'Taipei' => 'Asia/Taipei',
'W. Australia' => 'Australia/Perth',
'Korea' => 'Asia/Seoul',
'Tokyo' => 'Asia/Tokyo',
'Yakutsk' => 'Asia/Yakutsk',
'AUS Central' => 'Australia/Darwin',
'Cen. Australia' => 'Australia/Adelaide',
'AUS Eastern' => 'Australia/Sydney',
'E. Australia' => 'Australia/Brisbane',
'Tasmania' => 'Australia/Hobart',
'Vladivostok' => 'Asia/Vladivostok',
'West Pacific' => 'Pacific/Guam',
'Central Pacific' => 'Asia/Magadan',
'Fiji' => 'Pacific/Fiji',
'New Zealand' => 'Pacific/Auckland',
'Tonga' => 'Pacific/Tongatapu',
];

View file

@ -0,0 +1,152 @@
<?php
/**
* A list of additional PHP timezones that are returned by
* DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)
* valid for new DateTimeZone().
*
* This list does not include those timezone identifiers that we have to map to
* a different identifier for some PHP versions (see php-workaround.php).
*
* Instead of using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)
* directly, we use this file because DateTimeZone::ALL_WITH_BC is not properly
* supported by all PHP version and HHVM.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
return [
'Africa/Asmera',
'Africa/Timbuktu',
'America/Argentina/ComodRivadavia',
'America/Atka',
'America/Buenos_Aires',
'America/Catamarca',
'America/Coral_Harbour',
'America/Cordoba',
'America/Ensenada',
'America/Fort_Wayne',
'America/Indianapolis',
'America/Jujuy',
'America/Knox_IN',
'America/Louisville',
'America/Mendoza',
'America/Montreal',
'America/Porto_Acre',
'America/Rosario',
'America/Shiprock',
'America/Virgin',
'Antarctica/South_Pole',
'Asia/Ashkhabad',
'Asia/Calcutta',
'Asia/Chungking',
'Asia/Dacca',
'Asia/Istanbul',
'Asia/Katmandu',
'Asia/Macao',
'Asia/Saigon',
'Asia/Tel_Aviv',
'Asia/Thimbu',
'Asia/Ujung_Pandang',
'Asia/Ulan_Bator',
'Atlantic/Faeroe',
'Atlantic/Jan_Mayen',
'Australia/ACT',
'Australia/Canberra',
'Australia/LHI',
'Australia/North',
'Australia/NSW',
'Australia/Queensland',
'Australia/South',
'Australia/Tasmania',
'Australia/Victoria',
'Australia/West',
'Australia/Yancowinna',
'Brazil/Acre',
'Brazil/DeNoronha',
'Brazil/East',
'Brazil/West',
'Canada/Atlantic',
'Canada/Central',
'Canada/Eastern',
'Canada/Mountain',
'Canada/Newfoundland',
'Canada/Pacific',
'Canada/Saskatchewan',
'Canada/Yukon',
'CET',
'Chile/Continental',
'Chile/EasterIsland',
'EET',
'EST',
'Etc/GMT',
'Etc/GMT+0',
'Etc/GMT+1',
'Etc/GMT+10',
'Etc/GMT+11',
'Etc/GMT+12',
'Etc/GMT+2',
'Etc/GMT+3',
'Etc/GMT+4',
'Etc/GMT+5',
'Etc/GMT+6',
'Etc/GMT+7',
'Etc/GMT+8',
'Etc/GMT+9',
'Etc/GMT-0',
'Etc/GMT-1',
'Etc/GMT-10',
'Etc/GMT-11',
'Etc/GMT-12',
'Etc/GMT-13',
'Etc/GMT-14',
'Etc/GMT-2',
'Etc/GMT-3',
'Etc/GMT-4',
'Etc/GMT-5',
'Etc/GMT-6',
'Etc/GMT-7',
'Etc/GMT-8',
'Etc/GMT-9',
'Etc/GMT0',
'Etc/Greenwich',
'Etc/UCT',
'Etc/Universal',
'Etc/UTC',
'Etc/Zulu',
'Europe/Belfast',
'Europe/Nicosia',
'Europe/Tiraspol',
'GB',
'GMT',
'GMT+0',
'GMT-0',
'HST',
'MET',
'Mexico/BajaNorte',
'Mexico/BajaSur',
'Mexico/General',
'MST',
'NZ',
'Pacific/Ponape',
'Pacific/Samoa',
'Pacific/Truk',
'Pacific/Yap',
'PRC',
'ROC',
'ROK',
'UCT',
'US/Alaska',
'US/Aleutian',
'US/Arizona',
'US/Central',
'US/East-Indiana',
'US/Eastern',
'US/Hawaii',
'US/Indiana-Starke',
'US/Michigan',
'US/Mountain',
'US/Pacific',
'US/Samoa',
'WET',
];

View file

@ -0,0 +1,46 @@
<?php
/**
* A list of PHP timezones that were supported until 5.5.9, removed in
* PHP 5.5.10 and re-introduced in PHP 5.5.17.
*
* DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) returns them,
* but they are invalid for new DateTimeZone(). Fixed in PHP 5.5.17.
* https://bugs.php.net/bug.php?id=66985
*
* Some more info here:
* http://evertpot.com/php-5-5-10-timezone-changes/
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
return [
'CST6CDT' => 'America/Chicago',
'Cuba' => 'America/Havana',
'Egypt' => 'Africa/Cairo',
'Eire' => 'Europe/Dublin',
'EST5EDT' => 'America/New_York',
'Factory' => 'UTC',
'GB-Eire' => 'Europe/London',
'GMT0' => 'UTC',
'Greenwich' => 'UTC',
'Hongkong' => 'Asia/Hong_Kong',
'Iceland' => 'Atlantic/Reykjavik',
'Iran' => 'Asia/Tehran',
'Israel' => 'Asia/Jerusalem',
'Jamaica' => 'America/Jamaica',
'Japan' => 'Asia/Tokyo',
'Kwajalein' => 'Pacific/Kwajalein',
'Libya' => 'Africa/Tripoli',
'MST7MDT' => 'America/Denver',
'Navajo' => 'America/Denver',
'NZ-CHAT' => 'Pacific/Chatham',
'Poland' => 'Europe/Warsaw',
'Portugal' => 'Europe/Lisbon',
'PST8PDT' => 'America/Los_Angeles',
'Singapore' => 'Asia/Singapore',
'Turkey' => 'Europe/Istanbul',
'Universal' => 'UTC',
'W-SU' => 'Europe/Moscow',
'Zulu' => 'UTC',
];

View file

@ -0,0 +1,152 @@
<?php
/**
* Automatically generated timezone file.
*
* Last update: 2020-12-13T17:38:12+05:45
* Source: https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/).
* @license http://sabre.io/license/ Modified BSD License
*/
return [
'AUS Central Standard Time' => 'Australia/Darwin',
'AUS Eastern Standard Time' => 'Australia/Sydney',
'Afghanistan Standard Time' => 'Asia/Kabul',
'Alaskan Standard Time' => 'America/Anchorage',
'Aleutian Standard Time' => 'America/Adak',
'Altai Standard Time' => 'Asia/Barnaul',
'Arab Standard Time' => 'Asia/Riyadh',
'Arabian Standard Time' => 'Asia/Dubai',
'Arabic Standard Time' => 'Asia/Baghdad',
'Argentina Standard Time' => 'America/Buenos_Aires',
'Astrakhan Standard Time' => 'Europe/Astrakhan',
'Atlantic Standard Time' => 'America/Halifax',
'Aus Central W. Standard Time' => 'Australia/Eucla',
'Azerbaijan Standard Time' => 'Asia/Baku',
'Azores Standard Time' => 'Atlantic/Azores',
'Bahia Standard Time' => 'America/Bahia',
'Bangladesh Standard Time' => 'Asia/Dhaka',
'Belarus Standard Time' => 'Europe/Minsk',
'Bougainville Standard Time' => 'Pacific/Bougainville',
'Canada Central Standard Time' => 'America/Regina',
'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
'Caucasus Standard Time' => 'Asia/Yerevan',
'Cen. Australia Standard Time' => 'Australia/Adelaide',
'Central America Standard Time' => 'America/Guatemala',
'Central Asia Standard Time' => 'Asia/Almaty',
'Central Brazilian Standard Time' => 'America/Cuiaba',
'Central Europe Standard Time' => 'Europe/Budapest',
'Central European Standard Time' => 'Europe/Warsaw',
'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
'Central Standard Time' => 'America/Chicago',
'Central Standard Time (Mexico)' => 'America/Mexico_City',
'Chatham Islands Standard Time' => 'Pacific/Chatham',
'China Standard Time' => 'Asia/Shanghai',
'Cuba Standard Time' => 'America/Havana',
'Dateline Standard Time' => 'Etc/GMT+12',
'E. Africa Standard Time' => 'Africa/Nairobi',
'E. Australia Standard Time' => 'Australia/Brisbane',
'E. Europe Standard Time' => 'Europe/Chisinau',
'E. South America Standard Time' => 'America/Sao_Paulo',
'Easter Island Standard Time' => 'Pacific/Easter',
'Eastern Standard Time' => 'America/New_York',
'Eastern Standard Time (Mexico)' => 'America/Cancun',
'Egypt Standard Time' => 'Africa/Cairo',
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
'FLE Standard Time' => 'Europe/Kiev',
'Fiji Standard Time' => 'Pacific/Fiji',
'GMT Standard Time' => 'Europe/London',
'GTB Standard Time' => 'Europe/Bucharest',
'Georgian Standard Time' => 'Asia/Tbilisi',
'Greenland Standard Time' => 'America/Godthab',
'Greenwich Standard Time' => 'Atlantic/Reykjavik',
'Haiti Standard Time' => 'America/Port-au-Prince',
'Hawaiian Standard Time' => 'Pacific/Honolulu',
'India Standard Time' => 'Asia/Calcutta',
'Iran Standard Time' => 'Asia/Tehran',
'Israel Standard Time' => 'Asia/Jerusalem',
'Jordan Standard Time' => 'Asia/Amman',
'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
'Korea Standard Time' => 'Asia/Seoul',
'Libya Standard Time' => 'Africa/Tripoli',
'Line Islands Standard Time' => 'Pacific/Kiritimati',
'Lord Howe Standard Time' => 'Australia/Lord_Howe',
'Magadan Standard Time' => 'Asia/Magadan',
'Magallanes Standard Time' => 'America/Punta_Arenas',
'Marquesas Standard Time' => 'Pacific/Marquesas',
'Mauritius Standard Time' => 'Indian/Mauritius',
'Middle East Standard Time' => 'Asia/Beirut',
'Montevideo Standard Time' => 'America/Montevideo',
'Morocco Standard Time' => 'Africa/Casablanca',
'Mountain Standard Time' => 'America/Denver',
'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
'Myanmar Standard Time' => 'Asia/Rangoon',
'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
'Namibia Standard Time' => 'Africa/Windhoek',
'Nepal Standard Time' => 'Asia/Katmandu',
'New Zealand Standard Time' => 'Pacific/Auckland',
'Newfoundland Standard Time' => 'America/St_Johns',
'Norfolk Standard Time' => 'Pacific/Norfolk',
'North Asia East Standard Time' => 'Asia/Irkutsk',
'North Asia Standard Time' => 'Asia/Krasnoyarsk',
'North Korea Standard Time' => 'Asia/Pyongyang',
'Omsk Standard Time' => 'Asia/Omsk',
'Pacific SA Standard Time' => 'America/Santiago',
'Pacific Standard Time' => 'America/Los_Angeles',
'Pacific Standard Time (Mexico)' => 'America/Tijuana',
'Pakistan Standard Time' => 'Asia/Karachi',
'Paraguay Standard Time' => 'America/Asuncion',
'Qyzylorda Standard Time' => 'Asia/Qyzylorda',
'Romance Standard Time' => 'Europe/Paris',
'Russia Time Zone 10' => 'Asia/Srednekolymsk',
'Russia Time Zone 11' => 'Asia/Kamchatka',
'Russia Time Zone 3' => 'Europe/Samara',
'Russian Standard Time' => 'Europe/Moscow',
'SA Eastern Standard Time' => 'America/Cayenne',
'SA Pacific Standard Time' => 'America/Bogota',
'SA Western Standard Time' => 'America/La_Paz',
'SE Asia Standard Time' => 'Asia/Bangkok',
'Saint Pierre Standard Time' => 'America/Miquelon',
'Sakhalin Standard Time' => 'Asia/Sakhalin',
'Samoa Standard Time' => 'Pacific/Apia',
'Sao Tome Standard Time' => 'Africa/Sao_Tome',
'Saratov Standard Time' => 'Europe/Saratov',
'Singapore Standard Time' => 'Asia/Singapore',
'South Africa Standard Time' => 'Africa/Johannesburg',
'Sri Lanka Standard Time' => 'Asia/Colombo',
'Sudan Standard Time' => 'Africa/Khartoum',
'Syria Standard Time' => 'Asia/Damascus',
'Taipei Standard Time' => 'Asia/Taipei',
'Tasmania Standard Time' => 'Australia/Hobart',
'Tocantins Standard Time' => 'America/Araguaina',
'Tokyo Standard Time' => 'Asia/Tokyo',
'Tomsk Standard Time' => 'Asia/Tomsk',
'Tonga Standard Time' => 'Pacific/Tongatapu',
'Transbaikal Standard Time' => 'Asia/Chita',
'Turkey Standard Time' => 'Europe/Istanbul',
'Turks And Caicos Standard Time' => 'America/Grand_Turk',
'US Eastern Standard Time' => 'America/Indianapolis',
'US Mountain Standard Time' => 'America/Phoenix',
'UTC' => 'Etc/GMT',
'UTC+12' => 'Etc/GMT-12',
'UTC+13' => 'Etc/GMT-13',
'UTC-02' => 'Etc/GMT+2',
'UTC-08' => 'Etc/GMT+8',
'UTC-09' => 'Etc/GMT+9',
'UTC-11' => 'Etc/GMT+11',
'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
'Venezuela Standard Time' => 'America/Caracas',
'Vladivostok Standard Time' => 'Asia/Vladivostok',
'Volgograd Standard Time' => 'Europe/Volgograd',
'W. Australia Standard Time' => 'Australia/Perth',
'W. Central Africa Standard Time' => 'Africa/Lagos',
'W. Europe Standard Time' => 'Europe/Berlin',
'W. Mongolia Standard Time' => 'Asia/Hovd',
'West Asia Standard Time' => 'Asia/Tashkent',
'West Bank Standard Time' => 'Asia/Hebron',
'West Pacific Standard Time' => 'Pacific/Port_Moresby',
'Yakutsk Standard Time' => 'Asia/Yakutsk',
'Yukon Standard Time' => 'America/Whitehorse',
];

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml;
/**
* Context Stack.
*
* The Context maintains information about a document during either reading or
* writing.
*
* During this process, it may be necessary to override this context
* information.
*
* This trait allows easy access to the context, and allows the end-user to
* override its settings for document fragments, and easily restore it again
* later.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
trait ContextStackTrait
{
/**
* This is the element map. It contains a list of XML elements (in clark
* notation) as keys and PHP class names as values.
*
* The PHP class names must implement Sabre\Xml\Element.
*
* Values may also be a callable. In that case the function will be called
* directly.
*
* @var array
*/
public $elementMap = [];
/**
* A contextUri pointing to the document being parsed / written.
* This uri may be used to resolve relative urls that may appear in the
* document.
*
* The reader and writer don't use this property, but as it's an extremely
* common use-case for parsing XML documents, it's added here as a
* convenience.
*
* @var string|null
*/
public $contextUri;
/**
* This is a list of namespaces that you want to give default prefixes.
*
* You must make sure you create this entire list before starting to write.
* They should be registered on the root element.
*
* @var array
*/
public $namespaceMap = [];
/**
* This is a list of custom serializers for specific classes.
*
* The writer may use this if you attempt to serialize an object with a
* class that does not implement XmlSerializable.
*
* Instead it will look at this classmap to see if there is a custom
* serializer here. This is useful if you don't want your value objects
* to be responsible for serializing themselves.
*
* The keys in this classmap need to be fully qualified PHP class names,
* the values must be callbacks. The callbacks take two arguments. The
* writer class, and the value that must be written.
*
* function (Writer $writer, object $value)
*
* @var array
*/
public $classMap = [];
/**
* Backups of previous contexts.
*
* @var array
*/
protected $contextStack = [];
/**
* Create a new "context".
*
* This allows you to safely modify the elementMap, contextUri or
* namespaceMap. After you're done, you can restore the old data again
* with popContext.
*/
public function pushContext()
{
$this->contextStack[] = [
$this->elementMap,
$this->contextUri,
$this->namespaceMap,
$this->classMap,
];
}
/**
* Restore the previous "context".
*/
public function popContext()
{
list(
$this->elementMap,
$this->contextUri,
$this->namespaceMap,
$this->classMap
) = array_pop($this->contextStack);
}
}

View file

@ -0,0 +1,359 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Deserializer;
use Sabre\Xml\Reader;
/**
* This class provides a number of 'deserializer' helper functions.
* These can be used to easily specify custom deserializers for specific
* XML elements.
*
* You can either use these functions from within the $elementMap in the
* Service or Reader class, or you can call them from within your own
* deserializer functions.
*/
/**
* The 'keyValue' deserializer parses all child elements, and outputs them as
* a "key=>value" array.
*
* For example, keyvalue will parse:
*
* <?xml version="1.0"?>
* <s:root xmlns:s="http://sabredav.org/ns">
* <s:elem1>value1</s:elem1>
* <s:elem2>value2</s:elem2>
* <s:elem3 />
* </s:root>
*
* Into:
*
* [
* "{http://sabredav.org/ns}elem1" => "value1",
* "{http://sabredav.org/ns}elem2" => "value2",
* "{http://sabredav.org/ns}elem3" => null,
* ];
*
* If you specify the 'namespace' argument, the deserializer will remove
* the namespaces of the keys that match that namespace.
*
* For example, if you call keyValue like this:
*
* keyValue($reader, 'http://sabredav.org/ns')
*
* it's output will instead be:
*
* [
* "elem1" => "value1",
* "elem2" => "value2",
* "elem3" => null,
* ];
*
* Attributes will be removed from the top-level elements. If elements with
* the same name appear twice in the list, only the last one will be kept.
*/
function keyValue(Reader $reader, string $namespace = null): array
{
// If there's no children, we don't do anything.
if ($reader->isEmptyElement) {
$reader->next();
return [];
}
if (!$reader->read()) {
$reader->next();
return [];
}
if (Reader::END_ELEMENT === $reader->nodeType) {
$reader->next();
return [];
}
$values = [];
do {
if (Reader::ELEMENT === $reader->nodeType) {
if (null !== $namespace && $reader->namespaceURI === $namespace) {
$values[$reader->localName] = $reader->parseCurrentElement()['value'];
} else {
$clark = $reader->getClark();
$values[$clark] = $reader->parseCurrentElement()['value'];
}
} else {
if (!$reader->read()) {
break;
}
}
} while (Reader::END_ELEMENT !== $reader->nodeType);
$reader->read();
return $values;
}
/**
* The 'enum' deserializer parses elements into a simple list
* without values or attributes.
*
* For example, Elements will parse:
*
* <?xml version="1.0"? >
* <s:root xmlns:s="http://sabredav.org/ns">
* <s:elem1 />
* <s:elem2 />
* <s:elem3 />
* <s:elem4>content</s:elem4>
* <s:elem5 attr="val" />
* </s:root>
*
* Into:
*
* [
* "{http://sabredav.org/ns}elem1",
* "{http://sabredav.org/ns}elem2",
* "{http://sabredav.org/ns}elem3",
* "{http://sabredav.org/ns}elem4",
* "{http://sabredav.org/ns}elem5",
* ];
*
* This is useful for 'enum'-like structures.
*
* If the $namespace argument is specified, it will strip the namespace
* for all elements that match that.
*
* For example,
*
* enum($reader, 'http://sabredav.org/ns')
*
* would return:
*
* [
* "elem1",
* "elem2",
* "elem3",
* "elem4",
* "elem5",
* ];
*
* @return string[]
*/
function enum(Reader $reader, string $namespace = null): array
{
// If there's no children, we don't do anything.
if ($reader->isEmptyElement) {
$reader->next();
return [];
}
if (!$reader->read()) {
$reader->next();
return [];
}
if (Reader::END_ELEMENT === $reader->nodeType) {
$reader->next();
return [];
}
$currentDepth = $reader->depth;
$values = [];
do {
if (Reader::ELEMENT !== $reader->nodeType) {
continue;
}
if (!is_null($namespace) && $namespace === $reader->namespaceURI) {
$values[] = $reader->localName;
} else {
$values[] = (string) $reader->getClark();
}
} while ($reader->depth >= $currentDepth && $reader->next());
$reader->next();
return $values;
}
/**
* The valueObject deserializer turns an xml element into a PHP object of
* a specific class.
*
* This is primarily used by the mapValueObject function from the Service
* class, but it can also easily be used for more specific situations.
*
* @return object
*/
function valueObject(Reader $reader, string $className, string $namespace)
{
$valueObject = new $className();
if ($reader->isEmptyElement) {
$reader->next();
return $valueObject;
}
$defaultProperties = get_class_vars($className);
$reader->read();
do {
if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) {
if (property_exists($valueObject, $reader->localName)) {
if (is_array($defaultProperties[$reader->localName])) {
$valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value'];
} else {
$valueObject->{$reader->localName} = $reader->parseCurrentElement()['value'];
}
} else {
// Ignore property
$reader->next();
}
} else {
if (!$reader->read()) {
break;
}
}
} while (Reader::END_ELEMENT !== $reader->nodeType);
$reader->read();
return $valueObject;
}
/**
* This deserializer helps you deserialize xml structures that look like
* this:.
*
* <collection>
* <item>...</item>
* <item>...</item>
* <item>...</item>
* </collection>
*
* Many XML documents use patterns like that, and this deserializer
* allow you to get all the 'items' as an array.
*
* In that previous example, you would register the deserializer as such:
*
* $reader->elementMap['{}collection'] = function($reader) {
* return repeatingElements($reader, '{}item');
* }
*
* The repeatingElements deserializer simply returns everything as an array.
*
* $childElementName must either be a a clark-notation element name, or if no
* namespace is used, the bare element name.
*/
function repeatingElements(Reader $reader, string $childElementName): array
{
if ('{' !== $childElementName[0]) {
$childElementName = '{}'.$childElementName;
}
$result = [];
foreach ($reader->parseGetElements() as $element) {
if ($element['name'] === $childElementName) {
$result[] = $element['value'];
}
}
return $result;
}
/**
* This deserializer helps you to deserialize structures which contain mixed content like this:.
*
* <p>some text <extref>and a inline tag</extref>and even more text</p>
*
* The above example will return
*
* [
* 'some text',
* [
* 'name' => '{}extref',
* 'value' => 'and a inline tag',
* 'attributes' => []
* ],
* 'and even more text'
* ]
*
* In strict XML documents you wont find this kind of markup but in html this is a quite common pattern.
*/
function mixedContent(Reader $reader): array
{
// If there's no children, we don't do anything.
if ($reader->isEmptyElement) {
$reader->next();
return [];
}
$previousDepth = $reader->depth;
$content = [];
$reader->read();
while (true) {
if (Reader::ELEMENT == $reader->nodeType) {
$content[] = $reader->parseCurrentElement();
} elseif ($reader->depth >= $previousDepth && in_array($reader->nodeType, [Reader::TEXT, Reader::CDATA, Reader::WHITESPACE])) {
$content[] = $reader->value;
$reader->read();
} elseif (Reader::END_ELEMENT == $reader->nodeType) {
// Ensuring we are moving the cursor after the end element.
$reader->read();
break;
} else {
$reader->read();
}
}
return $content;
}
/**
* The functionCaller deserializer turns an xml element into whatever your callable return.
*
* You can use, e.g., a named constructor (factory method) to create an object using
* this function.
*
* @return mixed
*/
function functionCaller(Reader $reader, callable $func, string $namespace)
{
if ($reader->isEmptyElement) {
$reader->next();
return null;
}
$funcArgs = [];
$func = is_string($func) && false !== strpos($func, '::') ? explode('::', $func) : $func;
$ref = is_array($func) ? new \ReflectionMethod($func[0], $func[1]) : new \ReflectionFunction($func);
foreach ($ref->getParameters() as $parameter) {
$funcArgs[$parameter->getName()] = null;
}
$reader->read();
do {
if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) {
if (array_key_exists($reader->localName, $funcArgs)) {
$funcArgs[$reader->localName] = $reader->parseCurrentElement()['value'];
} else {
// Ignore property
$reader->next();
}
} else {
$reader->read();
}
} while (Reader::END_ELEMENT !== $reader->nodeType);
$reader->read();
return $func(...array_values($funcArgs));
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml;
/**
* This is the XML element interface.
*
* Elements are responsible for serializing and deserializing part of an XML
* document into PHP values.
*
* It combines XmlSerializable and XmlDeserializable into one logical class
* that does both.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface Element extends XmlSerializable, XmlDeserializable
{
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Element;
use Sabre\Xml;
/**
* The Base XML element is the standard parser & generator that's used by the
* XML reader and writer.
*
* It spits out a simple PHP array structure during deserialization, that can
* also be directly injected back into Writer::write.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Base implements Xml\Element
{
/**
* PHP value to serialize.
*
* @var mixed
*/
protected $value;
/**
* Constructor.
*/
public function __construct($value = null)
{
$this->value = $value;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Xml\Writer $writer)
{
$writer->write($this->value);
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* Important note 2: You are responsible for advancing the reader to the
* next element. Not doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Xml\Reader $reader)
{
$subTree = $reader->parseInnerTree();
return $subTree;
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Element;
use Sabre\Xml;
/**
* CDATA element.
*
* This element allows you to easily inject CDATA.
*
* Note that we strongly recommend avoiding CDATA nodes, unless you definitely
* know what you're doing, or you're working with unchangeable systems that
* require CDATA.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Cdata implements Xml\XmlSerializable
{
/**
* CDATA element value.
*
* @var string
*/
protected $value;
/**
* Constructor.
*/
public function __construct(string $value)
{
$this->value = $value;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Xml\Writer $writer)
{
$writer->writeCData($this->value);
}
}

View file

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Element;
use Sabre\Xml;
use Sabre\Xml\Deserializer;
use Sabre\Xml\Serializer;
/**
* 'Elements' is a simple list of elements, without values or attributes.
* For example, Elements will parse:.
*
* <?xml version="1.0"?>
* <s:root xmlns:s="http://sabredav.org/ns">
* <s:elem1 />
* <s:elem2 />
* <s:elem3 />
* <s:elem4>content</s:elem4>
* <s:elem5 attr="val" />
* </s:root>
*
* Into:
*
* [
* "{http://sabredav.org/ns}elem1",
* "{http://sabredav.org/ns}elem2",
* "{http://sabredav.org/ns}elem3",
* "{http://sabredav.org/ns}elem4",
* "{http://sabredav.org/ns}elem5",
* ];
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Elements implements Xml\Element
{
/**
* Value to serialize.
*
* @var array
*/
protected $value;
/**
* Constructor.
*/
public function __construct(array $value = [])
{
$this->value = $value;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Xml\Writer $writer)
{
Serializer\enum($writer, $this->value);
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* Important note 2: You are responsible for advancing the reader to the
* next element. Not doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseSubTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Xml\Reader $reader)
{
return Deserializer\enum($reader);
}
}

View file

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Element;
use Sabre\Xml;
use Sabre\Xml\Deserializer;
/**
* 'KeyValue' parses out all child elements from a single node, and outputs a
* key=>value struct.
*
* Attributes will be removed, and duplicate child elements are discarded.
* Complex values within the elements will be parsed by the 'standard' parser.
*
* For example, KeyValue will parse:
*
* <?xml version="1.0"?>
* <s:root xmlns:s="http://sabredav.org/ns">
* <s:elem1>value1</s:elem1>
* <s:elem2>value2</s:elem2>
* <s:elem3 />
* </s:root>
*
* Into:
*
* [
* "{http://sabredav.org/ns}elem1" => "value1",
* "{http://sabredav.org/ns}elem2" => "value2",
* "{http://sabredav.org/ns}elem3" => null,
* ];
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class KeyValue implements Xml\Element
{
/**
* Value to serialize.
*
* @var array
*/
protected $value;
/**
* Constructor.
*/
public function __construct(array $value = [])
{
$this->value = $value;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Xml\Writer $writer)
{
$writer->write($this->value);
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* Important note 2: You are responsible for advancing the reader to the
* next element. Not doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Xml\Reader $reader)
{
return Deserializer\keyValue($reader);
}
}

View file

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Element;
use Sabre\Xml;
/**
* Uri element.
*
* This represents a single uri. An example of how this may be encoded:
*
* <link>/foo/bar</link>
* <d:href xmlns:d="DAV:">http://example.org/hi</d:href>
*
* If the uri is relative, it will be automatically expanded to an absolute
* url during writing and reading, if the contextUri property is set on the
* reader and/or writer.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Uri implements Xml\Element
{
/**
* Uri element value.
*
* @var string
*/
protected $value;
/**
* Constructor.
*
* @param string $value
*/
public function __construct($value)
{
$this->value = $value;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Xml\Writer $writer)
{
$writer->text(
\Sabre\Uri\resolve(
$writer->contextUri,
$this->value
)
);
}
/**
* This method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* Important note 2: You are responsible for advancing the reader to the
* next element. Not doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseSubTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Xml\Reader $reader)
{
return new self(
\Sabre\Uri\resolve(
(string) $reader->contextUri,
$reader->readText()
)
);
}
}

View file

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Element;
use Sabre\Xml\Element;
use Sabre\Xml\Reader;
use Sabre\Xml\Writer;
/**
* The XmlFragment element allows you to extract a portion of your xml tree,
* and get a well-formed xml string.
*
* This goes a bit beyond `innerXml` and friends, as we'll also match all the
* correct namespaces.
*
* Please note that the XML fragment:
*
* 1. Will not have an <?xml declaration.
* 2. Or a DTD
* 3. It will have all the relevant xmlns attributes.
* 4. It may not have a root element.
*/
class XmlFragment implements Element
{
/**
* The inner XML value.
*
* @var string
*/
protected $xml;
/**
* Constructor.
*/
public function __construct(string $xml)
{
$this->xml = $xml;
}
/**
* Returns the inner XML document.
*/
public function getXml(): string
{
return $this->xml;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
$reader = new Reader();
// Wrapping the xml in a container, so root-less values can still be
// parsed.
$xml = <<<XML
<?xml version="1.0"?>
<xml-fragment xmlns="http://sabre.io/ns">{$this->getXml()}</xml-fragment>
XML;
$reader->xml($xml);
while ($reader->read()) {
if ($reader->depth < 1) {
// Skipping the root node.
continue;
}
switch ($reader->nodeType) {
case Reader::ELEMENT:
$writer->startElement(
(string) $reader->getClark()
);
$empty = $reader->isEmptyElement;
while ($reader->moveToNextAttribute()) {
switch ($reader->namespaceURI) {
case '':
$writer->writeAttribute($reader->localName, $reader->value);
break;
case 'http://www.w3.org/2000/xmlns/':
// Skip namespace declarations
break;
default:
$writer->writeAttribute((string) $reader->getClark(), $reader->value);
break;
}
}
if ($empty) {
$writer->endElement();
}
break;
case Reader::CDATA:
case Reader::TEXT:
$writer->text(
$reader->value
);
break;
case Reader::END_ELEMENT:
$writer->endElement();
break;
}
}
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = new self($reader->readInnerXml());
$reader->next();
return $result;
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml;
use LibXMLError;
use Throwable;
/**
* This exception is thrown when the Reader runs into a parsing error.
*
* This exception effectively wraps 1 or more LibXMLError objects.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class LibXMLException extends ParseException
{
/**
* The error list.
*
* @var \LibXMLError[]
*/
protected $errors;
/**
* Creates the exception.
*
* You should pass a list of LibXMLError objects in its constructor.
*
* @param LibXMLError[] $errors
* @param Throwable $previousException
*/
public function __construct(array $errors, int $code = 0, Throwable $previousException = null)
{
$this->errors = $errors;
parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException);
}
/**
* Returns the LibXML errors.
*/
public function getErrors(): array
{
return $this->errors;
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml;
use Exception;
/**
* This is a base exception for any exception related to parsing xml files.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ParseException extends Exception
{
}

View file

@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml;
use XMLReader;
/**
* The Reader class expands upon PHP's built-in XMLReader.
*
* The intended usage, is to assign certain XML elements to PHP classes. These
* need to be registered using the $elementMap public property.
*
* After this is done, a single call to parse() will parse the entire document,
* and delegate sub-sections of the document to element classes.
*
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Reader extends XMLReader
{
use ContextStackTrait;
/**
* Returns the current nodename in clark-notation.
*
* For example: "{http://www.w3.org/2005/Atom}feed".
* Or if no namespace is defined: "{}feed".
*
* This method returns null if we're not currently on an element.
*
* @return string|null
*/
public function getClark()
{
if (!$this->localName) {
return null;
}
return '{'.$this->namespaceURI.'}'.$this->localName;
}
/**
* Reads the entire document.
*
* This function returns an array with the following three elements:
* * name - The root element name.
* * value - The value for the root element.
* * attributes - An array of attributes.
*
* This function will also disable the standard libxml error handler (which
* usually just results in PHP errors), and throw exceptions instead.
*/
public function parse(): array
{
$previousEntityState = null;
$shouldCallLibxmlDisableEntityLoader = (\LIBXML_VERSION < 20900);
if ($shouldCallLibxmlDisableEntityLoader) {
$previousEntityState = libxml_disable_entity_loader(true);
}
$previousSetting = libxml_use_internal_errors(true);
try {
while (self::ELEMENT !== $this->nodeType) {
if (!$this->read()) {
$errors = libxml_get_errors();
libxml_clear_errors();
if ($errors) {
throw new LibXMLException($errors);
}
}
}
$result = $this->parseCurrentElement();
// last line of defense in case errors did occur above
$errors = libxml_get_errors();
libxml_clear_errors();
if ($errors) {
throw new LibXMLException($errors);
}
} finally {
libxml_use_internal_errors($previousSetting);
if ($shouldCallLibxmlDisableEntityLoader) {
libxml_disable_entity_loader($previousEntityState);
}
}
return $result;
}
/**
* parseGetElements parses everything in the current sub-tree,
* and returns an array of elements.
*
* Each element has a 'name', 'value' and 'attributes' key.
*
* If the element didn't contain sub-elements, an empty array is always
* returned. If there was any text inside the element, it will be
* discarded.
*
* If the $elementMap argument is specified, the existing elementMap will
* be overridden while parsing the tree, and restored after this process.
*/
public function parseGetElements(array $elementMap = null): array
{
$result = $this->parseInnerTree($elementMap);
if (!is_array($result)) {
return [];
}
return $result;
}
/**
* Parses all elements below the current element.
*
* This method will return a string if this was a text-node, or an array if
* there were sub-elements.
*
* If there's both text and sub-elements, the text will be discarded.
*
* If the $elementMap argument is specified, the existing elementMap will
* be overridden while parsing the tree, and restored after this process.
*
* @return array|string|null
*/
public function parseInnerTree(array $elementMap = null)
{
$text = null;
$elements = [];
if (self::ELEMENT === $this->nodeType && $this->isEmptyElement) {
// Easy!
$this->next();
return null;
}
if (!is_null($elementMap)) {
$this->pushContext();
$this->elementMap = $elementMap;
}
try {
if (!$this->read()) {
$errors = libxml_get_errors();
libxml_clear_errors();
if ($errors) {
throw new LibXMLException($errors);
}
throw new ParseException('This should never happen (famous last words)');
}
$keepOnParsing = true;
while ($keepOnParsing) {
if (!$this->isValid()) {
$errors = libxml_get_errors();
if ($errors) {
libxml_clear_errors();
throw new LibXMLException($errors);
}
}
switch ($this->nodeType) {
case self::ELEMENT:
$elements[] = $this->parseCurrentElement();
break;
case self::TEXT:
case self::CDATA:
$text .= $this->value;
$this->read();
break;
case self::END_ELEMENT:
// Ensuring we are moving the cursor after the end element.
$this->read();
$keepOnParsing = false;
break;
case self::NONE:
throw new ParseException('We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.');
default:
// Advance to the next element
$this->read();
break;
}
}
} finally {
if (!is_null($elementMap)) {
$this->popContext();
}
}
return $elements ? $elements : $text;
}
/**
* Reads all text below the current element, and returns this as a string.
*/
public function readText(): string
{
$result = '';
$previousDepth = $this->depth;
while ($this->read() && $this->depth != $previousDepth) {
if (in_array($this->nodeType, [XMLReader::TEXT, XMLReader::CDATA, XMLReader::WHITESPACE])) {
$result .= $this->value;
}
}
return $result;
}
/**
* Parses the current XML element.
*
* This method returns arn array with 3 properties:
* * name - A clark-notation XML element name.
* * value - The parsed value.
* * attributes - A key-value list of attributes.
*/
public function parseCurrentElement(): array
{
$name = $this->getClark();
$attributes = [];
if ($this->hasAttributes) {
$attributes = $this->parseAttributes();
}
$value = call_user_func(
$this->getDeserializerForElementName((string) $name),
$this
);
return [
'name' => $name,
'value' => $value,
'attributes' => $attributes,
];
}
/**
* Grabs all the attributes from the current element, and returns them as a
* key-value array.
*
* If the attributes are part of the same namespace, they will simply be
* short keys. If they are defined on a different namespace, the attribute
* name will be returned in clark-notation.
*/
public function parseAttributes(): array
{
$attributes = [];
while ($this->moveToNextAttribute()) {
if ($this->namespaceURI) {
// Ignoring 'xmlns', it doesn't make any sense.
if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) {
continue;
}
$name = $this->getClark();
$attributes[$name] = $this->value;
} else {
$attributes[$this->localName] = $this->value;
}
}
$this->moveToElement();
return $attributes;
}
/**
* Returns the function that should be used to parse the element identified
* by its clark-notation name.
*/
public function getDeserializerForElementName(string $name): callable
{
if (!array_key_exists($name, $this->elementMap)) {
if ('{}' == substr($name, 0, 2) && array_key_exists(substr($name, 2), $this->elementMap)) {
$name = substr($name, 2);
} else {
return ['Sabre\\Xml\\Element\\Base', 'xmlDeserialize'];
}
}
$deserializer = $this->elementMap[$name];
if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) {
return [$deserializer, 'xmlDeserialize'];
}
if (is_callable($deserializer)) {
return $deserializer;
}
$type = gettype($deserializer);
if ('string' === $type) {
$type .= ' ('.$deserializer.')';
} elseif ('object' === $type) {
$type .= ' ('.get_class($deserializer).')';
}
throw new \LogicException('Could not use this type as a deserializer: '.$type.' for element: '.$name);
}
}

View file

@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace Sabre\Xml\Serializer;
use InvalidArgumentException;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* This file provides a number of 'serializer' helper functions.
*
* These helper functions can be used to easily xml-encode common PHP
* data structures, or can be placed in the $classMap.
*/
/**
* The 'enum' serializer writes simple list of elements.
*
* For example, calling:
*
* enum($writer, [
* "{http://sabredav.org/ns}elem1",
* "{http://sabredav.org/ns}elem2",
* "{http://sabredav.org/ns}elem3",
* "{http://sabredav.org/ns}elem4",
* "{http://sabredav.org/ns}elem5",
* ]);
*
* Will generate something like this (if the correct namespace is declared):
*
* <s:elem1 />
* <s:elem2 />
* <s:elem3 />
* <s:elem4>content</s:elem4>
* <s:elem5 attr="val" />
*
* @param string[] $values
*/
function enum(Writer $writer, array $values)
{
foreach ($values as $value) {
$writer->writeElement($value);
}
}
/**
* The valueObject serializer turns a simple PHP object into a classname.
*
* Every public property will be encoded as an xml element with the same
* name, in the XML namespace as specified.
*
* Values that are set to null or an empty array are not serialized. To
* serialize empty properties, you must specify them as an empty string.
*
* @param object $valueObject
*/
function valueObject(Writer $writer, $valueObject, string $namespace)
{
foreach (get_object_vars($valueObject) as $key => $val) {
if (is_array($val)) {
// If $val is an array, it has a special meaning. We need to
// generate one child element for each item in $val
foreach ($val as $child) {
$writer->writeElement('{'.$namespace.'}'.$key, $child);
}
} elseif (null !== $val) {
$writer->writeElement('{'.$namespace.'}'.$key, $val);
}
}
}
/**
* This serializer helps you serialize xml structures that look like
* this:.
*
* <collection>
* <item>...</item>
* <item>...</item>
* <item>...</item>
* </collection>
*
* In that previous example, this serializer just serializes the item element,
* and this could be called like this:
*
* repeatingElements($writer, $items, '{}item');
*/
function repeatingElements(Writer $writer, array $items, string $childElementName)
{
foreach ($items as $item) {
$writer->writeElement($childElementName, $item);
}
}
/**
* This function is the 'default' serializer that is able to serialize most
* things, and delegates to other serializers if needed.
*
* The standardSerializer supports a wide-array of values.
*
* $value may be a string or integer, it will just write out the string as text.
* $value may be an instance of XmlSerializable or Element, in which case it
* calls it's xmlSerialize() method.
* $value may be a PHP callback/function/closure, in case we call the callback
* and give it the Writer as an argument.
* $value may be a an object, and if it's in the classMap we automatically call
* the correct serializer for it.
* $value may be null, in which case we do nothing.
*
* If $value is an array, the array must look like this:
*
* [
* [
* 'name' => '{namespaceUri}element-name',
* 'value' => '...',
* 'attributes' => [ 'attName' => 'attValue' ]
* ]
* [,
* 'name' => '{namespaceUri}element-name2',
* 'value' => '...',
* ]
* ]
*
* This would result in xml like:
*
* <element-name xmlns="namespaceUri" attName="attValue">
* ...
* </element-name>
* <element-name2>
* ...
* </element-name2>
*
* The value property may be any value standardSerializer supports, so you can
* nest data-structures this way. Both value and attributes are optional.
*
* Alternatively, you can also specify the array using this syntax:
*
* [
* [
* '{namespaceUri}element-name' => '...',
* '{namespaceUri}element-name2' => '...',
* ]
* ]
*
* This is excellent for simple key->value structures, and here you can also
* specify anything for the value.
*
* You can even mix the two array syntaxes.
*
* @param string|int|float|bool|array|object $value
*/
function standardSerializer(Writer $writer, $value)
{
if (is_scalar($value)) {
// String, integer, float, boolean
$writer->text((string) $value);
} elseif ($value instanceof XmlSerializable) {
// XmlSerializable classes or Element classes.
$value->xmlSerialize($writer);
} elseif (is_object($value) && isset($writer->classMap[get_class($value)])) {
// It's an object which class appears in the classmap.
$writer->classMap[get_class($value)]($writer, $value);
} elseif (is_callable($value)) {
// A callback
$value($writer);
} elseif (is_array($value) && array_key_exists('name', $value)) {
// if the array had a 'name' element, we assume that this array
// describes a 'name' and optionally 'attributes' and 'value'.
$name = $value['name'];
$attributes = isset($value['attributes']) ? $value['attributes'] : [];
$value = isset($value['value']) ? $value['value'] : null;
$writer->startElement($name);
$writer->writeAttributes($attributes);
$writer->write($value);
$writer->endElement();
} elseif (is_array($value)) {
foreach ($value as $name => $item) {
if (is_int($name)) {
// This item has a numeric index. We just loop through the
// array and throw it back in the writer.
standardSerializer($writer, $item);
} elseif (is_string($name) && is_array($item) && isset($item['attributes'])) {
// The key is used for a name, but $item has 'attributes' and
// possibly 'value'
$writer->startElement($name);
$writer->writeAttributes($item['attributes']);
if (isset($item['value'])) {
$writer->write($item['value']);
}
$writer->endElement();
} elseif (is_string($name)) {
// This was a plain key-value array.
$writer->startElement($name);
$writer->write($item);
$writer->endElement();
} else {
throw new InvalidArgumentException('The writer does not know how to serialize arrays with keys of type: '.gettype($name));
}
}
} elseif (is_object($value)) {
throw new InvalidArgumentException('The writer cannot serialize objects of class: '.get_class($value));
} elseif (!is_null($value)) {
throw new InvalidArgumentException('The writer cannot serialize values of type: '.gettype($value));
}
}

Some files were not shown because too many files have changed in this diff Show more