Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

761
vendor/sabre/vobject/lib/Cli.php vendored Normal file
View File

@@ -0,0 +1,761 @@
<?php
namespace Sabre\VObject;
use
InvalidArgumentException;
/**
* This is the CLI interface for sabre-vobject.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Cli {
/**
* No output
*
* @var bool
*/
protected $quiet = false;
/**
* Help display
*
* @var bool
*/
protected $showHelp = false;
/**
* Wether to spit out 'mimedir' or 'json' format.
*
* @var string
*/
protected $format;
/**
* JSON pretty print
*
* @var bool
*/
protected $pretty;
/**
* Source file
*
* @var string
*/
protected $inputPath;
/**
* Destination file
*
* @var string
*/
protected $outputPath;
/**
* output stream
*
* @var resource
*/
protected $stdout;
/**
* stdin
*
* @var resource
*/
protected $stdin;
/**
* stderr
*
* @var resource
*/
protected $stderr;
/**
* Input format (one of json or mimedir)
*
* @var string
*/
protected $inputFormat;
/**
* Makes the parser less strict.
*
* @var bool
*/
protected $forgiving = false;
/**
* Main function
*
* @return int
*/
public function main(array $argv) {
// @codeCoverageIgnoreStart
// We cannot easily test this, so we'll skip it. Pretty basic anyway.
if (!$this->stderr) {
$this->stderr = fopen('php://stderr', 'w');
}
if (!$this->stdout) {
$this->stdout = fopen('php://stdout', 'w');
}
if (!$this->stdin) {
$this->stdin = fopen('php://stdin', 'r');
}
// @codeCoverageIgnoreEnd
try {
list($options, $positional) = $this->parseArguments($argv);
if (isset($options['q'])) {
$this->quiet = true;
}
$this->log($this->colorize('green', "sabre/vobject ") . $this->colorize('yellow', Version::VERSION));
foreach($options as $name=>$value) {
switch($name) {
case 'q' :
// Already handled earlier.
break;
case 'h' :
case 'help' :
$this->showHelp();
return 0;
break;
case 'format' :
switch($value) {
// jcard/jcal documents
case 'jcard' :
case 'jcal' :
// specific document versions
case 'vcard21' :
case 'vcard30' :
case 'vcard40' :
case 'icalendar20' :
// specific formats
case 'json' :
case 'mimedir' :
// icalendar/vcad
case 'icalendar' :
case 'vcard' :
$this->format = $value;
break;
default :
throw new InvalidArgumentException('Unknown format: ' . $value);
}
break;
case 'pretty' :
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
$this->pretty = true;
}
break;
case 'forgiving' :
$this->forgiving = true;
break;
case 'inputformat' :
switch($value) {
// json formats
case 'jcard' :
case 'jcal' :
case 'json' :
$this->inputFormat = 'json';
break;
// mimedir formats
case 'mimedir' :
case 'icalendar' :
case 'vcard' :
case 'vcard21' :
case 'vcard30' :
case 'vcard40' :
case 'icalendar20' :
$this->inputFormat = 'mimedir';
break;
default :
throw new InvalidArgumentException('Unknown format: ' . $value);
}
break;
default :
throw new InvalidArgumentException('Unknown option: ' . $name);
}
}
if (count($positional) === 0) {
$this->showHelp();
return 1;
}
if (count($positional) === 1) {
throw new InvalidArgumentException('Inputfile is a required argument');
}
if (count($positional) > 3) {
throw new InvalidArgumentException('Too many arguments');
}
if (!in_array($positional[0], array('validate','repair','convert','color'))) {
throw new InvalidArgumentException('Uknown command: ' . $positional[0]);
}
} catch (InvalidArgumentException $e) {
$this->showHelp();
$this->log('Error: ' . $e->getMessage(), 'red');
return 1;
}
$command = $positional[0];
$this->inputPath = $positional[1];
$this->outputPath = isset($positional[2])?$positional[2]:'-';
if ($this->outputPath !== '-') {
$this->stdout = fopen($this->outputPath, 'w');
}
if (!$this->inputFormat) {
if (substr($this->inputPath, -5)==='.json') {
$this->inputFormat = 'json';
} else {
$this->inputFormat = 'mimedir';
}
}
if (!$this->format) {
if (substr($this->outputPath,-5)==='.json') {
$this->format = 'json';
} else {
$this->format = 'mimedir';
}
}
$realCode = 0;
try {
while($input = $this->readInput()) {
$returnCode = $this->$command($input);
if ($returnCode!==0) $realCode = $returnCode;
}
} catch (EofException $e) {
// end of file
} catch (\Exception $e) {
$this->log('Error: ' . $e->getMessage(),'red');
return 2;
}
return $realCode;
}
/**
* Shows the help message.
*
* @return void
*/
protected function showHelp() {
$this->log('Usage:', 'yellow');
$this->log(" vobject [options] command [arguments]");
$this->log('');
$this->log('Options:', 'yellow');
$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', ' --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(" 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(" must be specified here.");
// 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('Commands:', 'yellow');
$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', ' convert') . ' source_file [output_file] Converts a file.');
$this->log($this->colorize('green', ' color') . ' source_file Colorize a file, useful for debbugging.');
$this->log(
<<<HELP
If source_file is set as '-', STDIN will be used.
If output_file is omitted, STDOUT will be used.
All other output is sent to STDERR.
HELP
);
$this->log('Examples:', 'yellow');
$this->log(' vobject convert contact.vcf contact.json');
$this->log(' vobject convert --format=vcard40 old.vcf new.vcf');
$this->log(' vobject convert --inputformat=json --format=mimedir - -');
$this->log(' vobject color calendar.ics');
$this->log('');
$this->log('https://github.com/fruux/sabre-vobject','purple');
}
/**
* Validates a VObject file
*
* @param Component $vObj
* @return int
*/
protected function validate($vObj) {
$returnCode = 0;
switch($vObj->name) {
case 'VCALENDAR' :
$this->log("iCalendar: " . (string)$vObj->VERSION);
break;
case 'VCARD' :
$this->log("vCard: " . (string)$vObj->VERSION);
break;
}
$warnings = $vObj->validate();
if (!count($warnings)) {
$this->log(" No warnings!");
} else {
$levels = array(
1 => 'REPAIRED',
2 => 'WARNING',
3 => 'ERROR',
);
$returnCode = 2;
foreach($warnings as $warn) {
$extra = '';
if ($warn['node'] instanceof Property) {
$extra = ' (property: "' . $warn['node']->name . '")';
}
$this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra);
}
}
return $returnCode;
}
/**
* Repairs a VObject file
*
* @param Component $vObj
* @return int
*/
protected function repair($vObj) {
$returnCode = 0;
switch($vObj->name) {
case 'VCALENDAR' :
$this->log("iCalendar: " . (string)$vObj->VERSION);
break;
case 'VCARD' :
$this->log("vCard: " . (string)$vObj->VERSION);
break;
}
$warnings = $vObj->validate(Node::REPAIR);
if (!count($warnings)) {
$this->log(" No warnings!");
} else {
$levels = array(
1 => 'REPAIRED',
2 => 'WARNING',
3 => 'ERROR',
);
$returnCode = 2;
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());
return $returnCode;
}
/**
* Converts a vObject file to a new format.
*
* @param Component $vObj
* @return int
*/
protected function convert($vObj) {
$json = false;
$convertVersion = null;
$forceInput = null;
switch($this->format) {
case 'json' :
$json = true;
if ($vObj->name === 'VCARD') {
$convertVersion = Document::VCARD40;
}
break;
case 'jcard' :
$json = true;
$forceInput = 'VCARD';
$convertVersion = Document::VCARD40;
break;
case 'jcal' :
$json = true;
$forceInput = 'VCALENDAR';
break;
case 'mimedir' :
case 'icalendar' :
case 'icalendar20' :
case 'vcard' :
break;
case 'vcard21' :
$convertVersion = Document::VCARD21;
break;
case 'vcard30' :
$convertVersion = Document::VCARD30;
break;
case 'vcard40' :
$convertVersion = Document::VCARD40;
break;
}
if ($forceInput && $vObj->name !== $forceInput) {
throw new \Exception('You cannot convert a ' . strtolower($vObj->name) . ' to ' . $this->format);
}
if ($convertVersion) {
$vObj = $vObj->convert($convertVersion);
}
if ($json) {
$jsonOptions = 0;
if ($this->pretty) {
$jsonOptions = JSON_PRETTY_PRINT;
}
fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions));
} else {
fwrite($this->stdout, $vObj->serialize());
}
return 0;
}
/**
* Colorizes a file
*
* @param Component $vObj
* @return int
*/
protected function color($vObj) {
fwrite($this->stdout, $this->serializeComponent($vObj));
}
/**
* Returns an ansi color string for a color name.
*
* @param string $color
* @return string
*/
protected function colorize($color, $str, $resetTo = 'default') {
$colors = array(
'cyan' => '1;36',
'red' => '1;31',
'yellow' => '1;33',
'blue' => '0;34',
'green' => '0;32',
'default' => '0',
'purple' => '0;35',
);
return "\033[" . $colors[$color] . 'm' . $str . "\033[".$colors[$resetTo]."m";
}
/**
* Writes out a string in specific color.
*
* @param string $color
* @param string $str
* @return void
*/
protected function cWrite($color, $str) {
fwrite($this->stdout, $this->colorize($color, $str));
}
protected function serializeComponent(Component $vObj) {
$this->cWrite('cyan', 'BEGIN');
$this->cWrite('red', ':');
$this->cWrite('yellow', $vObj->name . "\n");
/**
* Gives a component a 'score' for sorting purposes.
*
* This is solely used by the childrenSort method.
*
* A higher score means the item will be lower in the list.
* To avoid score collisions, each "score category" has a reasonable
* space to accomodate elements. The $key is added to the $score to
* preserve the original relative order of elements.
*
* @param int $key
* @param array $array
* @return int
*/
$sortScore = function($key, $array) {
if ($array[$key] instanceof Component) {
// We want to encode VTIMEZONE first, this is a personal
// preference.
if ($array[$key]->name === 'VTIMEZONE') {
$score=300000000;
return $score+$key;
} else {
$score=400000000;
return $score+$key;
}
} else {
// Properties get encoded first
// VCARD version 4.0 wants the VERSION property to appear first
if ($array[$key] instanceof Property) {
if ($array[$key]->name === 'VERSION') {
$score=100000000;
return $score+$key;
} else {
// All other properties
$score=200000000;
return $score+$key;
}
}
}
};
$tmp = $vObj->children;
uksort(
$vObj->children,
function($a, $b) use ($sortScore, $tmp) {
$sA = $sortScore($a, $tmp);
$sB = $sortScore($b, $tmp);
return $sA - $sB;
}
);
foreach($vObj->children as $child) {
if ($child instanceof Component) {
$this->serializeComponent($child);
} else {
$this->serializeProperty($child);
}
}
$this->cWrite('cyan', 'END');
$this->cWrite('red', ':');
$this->cWrite('yellow', $vObj->name . "\n");
}
/**
* Colorizes a property.
*
* @param Property $property
* @return void
*/
protected function serializeProperty(Property $property) {
if ($property->group) {
$this->cWrite('default', $property->group);
$this->cWrite('red', '.');
}
$str = '';
$this->cWrite('yellow', $property->name);
foreach($property->parameters as $param) {
$this->cWrite('red',';');
$this->cWrite('blue', $param->serialize());
}
$this->cWrite('red',':');
if ($property instanceof Property\Binary) {
$this->cWrite('default', 'embedded binary stripped. (' . strlen($property->getValue()) . ' bytes)');
} else {
$parts = $property->getParts();
$first1 = true;
// Looping through property values
foreach($parts as $part) {
if ($first1) {
$first1 = false;
} else {
$this->cWrite('red', $property->delimiter);
}
$first2 = true;
// Looping through property sub-values
foreach((array)$part as $subPart) {
if ($first2) {
$first2 = false;
} else {
// The sub-value delimiter is always comma
$this->cWrite('red', ',');
}
$subPart = strtr(
$subPart,
array(
'\\' => $this->colorize('purple', '\\\\', 'green'),
';' => $this->colorize('purple', '\;', 'green'),
',' => $this->colorize('purple', '\,', 'green'),
"\n" => $this->colorize('purple', "\\n\n\t", 'green'),
"\r" => "",
)
);
$this->cWrite('green', $subPart);
}
}
}
$this->cWrite("default", "\n");
}
/**
* Parses the list of arguments.
*
* @param array $argv
* @return void
*/
protected function parseArguments(array $argv) {
$positional = array();
$options = array();
for($ii=0; $ii < count($argv); $ii++) {
// Skipping the first argument.
if ($ii===0) continue;
$v = $argv[$ii];
if (substr($v,0,2)==='--') {
// This is a long-form option.
$optionName = substr($v,2);
$optionValue = true;
if (strpos($optionName,'=')) {
list($optionName, $optionValue) = explode('=', $optionName);
}
$options[$optionName] = $optionValue;
} elseif (substr($v,0,1) === '-' && strlen($v)>1) {
// This is a short-form option.
foreach(str_split(substr($v,1)) as $option) {
$options[$option] = true;
}
} else {
$positional[] = $v;
}
}
return array($options, $positional);
}
protected $parser;
/**
* Reads the input file
*
* @return Component
*/
protected function readInput() {
if (!$this->parser) {
if ($this->inputPath!=='-') {
$this->stdin = fopen($this->inputPath,'r');
}
if ($this->inputFormat === 'mimedir') {
$this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving?Reader::OPTION_FORGIVING:0));
} else {
$this->parser = new Parser\Json($this->stdin, ($this->forgiving?Reader::OPTION_FORGIVING:0));
}
}
return $this->parser->parse();
}
/**
* Sends a message to STDERR.
*
* @param string $msg
* @return void
*/
protected function log($msg, $color = 'default') {
if (!$this->quiet) {
if ($color!=='default') {
$msg = $this->colorize($color, $msg);
}
fwrite($this->stderr, $msg . "\n");
}
}
}

595
vendor/sabre/vobject/lib/Component.php vendored Normal file
View File

@@ -0,0 +1,595 @@
<?php
namespace Sabre\VObject;
/**
* Component
*
* A component represents a group of properties, such as VCALENDAR, VEVENT, or
* VCARD.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Component extends Node {
/**
* Component name.
*
* This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD.
*
* @var string
*/
public $name;
/**
* A list of properties and/or sub-components.
*
* @var array
*/
public $children = array();
/**
* Creates a new component.
*
* You can specify the children either in key=>value syntax, in which case
* properties will automatically be created, or you can just pass a list of
* Component and Property object.
*
* By default, a set of sensible values will be added to the component. For
* an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
* ensure that this does not happen, set $defaults to false.
*
* @param Document $root
* @param string $name such as VCALENDAR, VEVENT.
* @param array $children
* @param bool $defaults
* @return void
*/
function __construct(Document $root, $name, array $children = array(), $defaults = true) {
$this->name = strtoupper($name);
$this->root = $root;
if ($defaults) {
// 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 = array();
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) {
if ($child instanceof Node) {
// Component or Property
$this->add($child);
} else {
// Property key=>value
$this->add($k, $child);
}
}
}
}
/**
* Adds a new property or component, and returns the new item.
*
* This method has 3 possible signatures:
*
* add(Component $comp) // Adds a new component
* add(Property $prop) // Adds a new property
* add($name, $value, array $parameters = array()) // Adds a new property
* add($name, array $children = array()) // Adds a new component
* by name.
*
* @return Node
*/
function add($a1, $a2 = null, $a3 = null) {
if ($a1 instanceof Node) {
if (!is_null($a2)) {
throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
}
$a1->parent = $this;
$this->children[] = $a1;
return $a1;
} elseif(is_string($a1)) {
$item = $this->root->create($a1, $a2, $a3);
$item->parent = $this;
$this->children[] = $item;
return $item;
} else {
throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
}
}
/**
* This method removes a component or property from this component.
*
* You can either specify the item by name (like DTSTART), in which case
* all properties/components with that name will be removed, or you can
* pass an instance of a property or component, in which case only that
* exact item will be removed.
*
* The removed item will be returned. In case there were more than 1 items
* removed, only the last one will be returned.
*
* @param mixed $item
* @return void
*/
function remove($item) {
if (is_string($item)) {
$children = $this->select($item);
foreach($children as $k=>$child) {
unset($this->children[$k]);
}
return $child;
} else {
foreach($this->children as $k => $child) {
if ($child===$item) {
unset($this->children[$k]);
return $child;
}
}
throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component');
}
}
/**
* Returns an iterable list of children
*
* @return array
*/
function children() {
return $this->children;
}
/**
* This method only returns a list of sub-components. Properties are
* ignored.
*
* @return array
*/
function getComponents() {
$result = array();
foreach($this->children as $child) {
if ($child instanceof Component) {
$result[] = $child;
}
}
return $result;
}
/**
* Returns an array with elements that match the specified name.
*
* This function is also aware of MIME-Directory groups (as they appear in
* vcards). This means that if a property is grouped as "HOME.EMAIL", it
* will also be returned when searching for just "EMAIL". If you want to
* search for a property in a specific group, you can select on the entire
* string ("HOME.EMAIL"). If you want to search on a specific property that
* 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
* @return array
*/
function select($name) {
$group = null;
$name = strtoupper($name);
if (strpos($name,'.')!==false) {
list($group,$name) = explode('.', $name, 2);
}
$result = array();
foreach($this->children as $key=>$child) {
if (
(
strtoupper($child->name) === $name
&& (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group))
)
||
(
$name === '' && $child instanceof Property && strtoupper($child->group) === $group
)
) {
$result[$key] = $child;
}
}
reset($result);
return $result;
}
/**
* Turns the object back into a serialized blob.
*
* @return string
*/
function serialize() {
$str = "BEGIN:" . $this->name . "\r\n";
/**
* Gives a component a 'score' for sorting purposes.
*
* This is solely used by the childrenSort method.
*
* A higher score means the item will be lower in the list.
* To avoid score collisions, each "score category" has a reasonable
* space to accomodate elements. The $key is added to the $score to
* preserve the original relative order of elements.
*
* @param int $key
* @param array $array
* @return int
*/
$sortScore = function($key, $array) {
if ($array[$key] instanceof Component) {
// We want to encode VTIMEZONE first, this is a personal
// preference.
if ($array[$key]->name === 'VTIMEZONE') {
$score=300000000;
return $score+$key;
} else {
$score=400000000;
return $score+$key;
}
} else {
// Properties get encoded first
// VCARD version 4.0 wants the VERSION property to appear first
if ($array[$key] instanceof Property) {
if ($array[$key]->name === 'VERSION') {
$score=100000000;
return $score+$key;
} else {
// All other properties
$score=200000000;
return $score+$key;
}
}
}
};
$tmp = $this->children;
uksort(
$this->children,
function($a, $b) use ($sortScore, $tmp) {
$sA = $sortScore($a, $tmp);
$sB = $sortScore($b, $tmp);
return $sA - $sB;
}
);
foreach($this->children as $child) $str.=$child->serialize();
$str.= "END:" . $this->name . "\r\n";
return $str;
}
/**
* This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents.
*
* @return array
*/
function jsonSerialize() {
$components = array();
$properties = array();
foreach($this->children as $child) {
if ($child instanceof Component) {
$components[] = $child->jsonSerialize();
} else {
$properties[] = $child->jsonSerialize();
}
}
return array(
strtolower($this->name),
$properties,
$components
);
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults() {
return array();
}
/* Magic property accessors {{{ */
/**
* Using 'get' you will either get a property or component.
*
* If there were no child-elements found with the specified name,
* null is returned.
*
* To use this, this may look something like this:
*
* $event = $calendar->VEVENT;
*
* @param string $name
* @return Property
*/
function __get($name) {
$matches = $this->select($name);
if (count($matches)===0) {
return null;
} 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.
*
* @param string $name
* @return bool
*/
function __isset($name) {
$matches = $this->select($name);
return count($matches)>0;
}
/**
* Using the setter method you can add properties or subcomponents
*
* You can either pass a Component, Property
* object, or a string to automatically create a Property.
*
* If the item already exists, it will be removed. If you want to add
* a new item with the same name, always use the add() method.
*
* @param string $name
* @param mixed $value
* @return void
*/
function __set($name, $value) {
$matches = $this->select($name);
$overWrite = count($matches)?key($matches):null;
if ($value instanceof Component || $value instanceof Property) {
$value->parent = $this;
if (!is_null($overWrite)) {
$this->children[$overWrite] = $value;
} else {
$this->children[] = $value;
}
} else {
$property = $this->root->create($name,$value);
$property->parent = $this;
if (!is_null($overWrite)) {
$this->children[$overWrite] = $property;
} else {
$this->children[] = $property;
}
}
}
/**
* Removes all properties and components within this component with the
* specified name.
*
* @param string $name
* @return void
*/
function __unset($name) {
$matches = $this->select($name);
foreach($matches as $k=>$child) {
unset($this->children[$k]);
$child->parent = null;
}
}
/* }}} */
/**
* This method is automatically called when the object is cloned.
* Specifically, this will ensure all child elements are also cloned.
*
* @return void
*/
function __clone() {
foreach($this->children as $key=>$child) {
$this->children[$key] = clone $child;
$this->children[$key]->parent = $this;
}
}
/**
* 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
*/
function getValidationRules() {
return array();
}
/**
* 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
*/
function validate($options = 0) {
$rules = $this->getValidationRules();
$defaults = $this->getDefaults();
$propertyCounters = array();
$messages = array();
foreach($this->children as $child) {
$name = strtoupper($child->name);
if (!isset($propertyCounters[$name])) {
$propertyCounters[$name] = 1;
} else {
$propertyCounters[$name]++;
}
$messages = array_merge($messages, $child->validate($options));
}
foreach($rules as $propName => $rule) {
switch($rule) {
case '0' :
if (isset($propertyCounters[$propName])) {
$messages[] = array(
'level' => 3,
'message' => $propName . ' MUST NOT appear in a ' . $this->name . ' component',
'node' => $this,
);
}
break;
case '1' :
if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName]!==1) {
$repaired = false;
if ($options & self::REPAIR && isset($defaults[$propName])) {
$this->add($propName, $defaults[$propName]);
}
$messages[] = array(
'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[] = array(
'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) {
$messages[] = array(
'level' => 3,
'message' => $propName . ' MUST NOT appear more than once in a ' . $this->name . ' component',
'node' => $this,
);
}
break;
}
}
return $messages;
}
}

View File

@@ -0,0 +1,108 @@
<?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 {
/**
* 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
*/
function getValidationRules() {
return array(
'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
*/
function validate($options = 0) {
$result = parent::validate($options);
if (isset($this->DTEND) && isset($this->DURATION)) {
$result[] = array(
'level' => 3,
'message' => 'DTEND and DURATION cannot both be present',
'node' => $this
);
}
if (isset($this->DURATION) && !isset($this->DTSTART)) {
$result[] = array(
'level' => 3,
'message' => 'DURATION must be declared with a DTSTART.',
'node' => $this
);
}
return $result;
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
/**
* VAlarm component
*
* This component contains some additional functionality specific for VALARMs.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VAlarm extends VObject\Component {
/**
* Returns a DateTime object when this alarm is going to trigger.
*
* This ignores repeated alarm, only the first trigger is returned.
*
* @return DateTime
*/
public function getEffectiveTriggerTime() {
$trigger = $this->TRIGGER;
if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') {
$triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
$related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START';
$parentComponent = $this->parent;
if ($related === 'START') {
if ($parentComponent->name === 'VTODO') {
$propName = 'DUE';
} else {
$propName = 'DTSTART';
}
$effectiveTrigger = clone $parentComponent->$propName->getDateTime();
$effectiveTrigger->add($triggerDuration);
} else {
if ($parentComponent->name === 'VTODO') {
$endProp = 'DUE';
} elseif ($parentComponent->name === 'VEVENT') {
$endProp = 'DTEND';
} else {
throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT');
}
if (isset($parentComponent->$endProp)) {
$effectiveTrigger = clone $parentComponent->$endProp->getDateTime();
$effectiveTrigger->add($triggerDuration);
} elseif (isset($parentComponent->DURATION)) {
$effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
$duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
$effectiveTrigger->add($duration);
$effectiveTrigger->add($triggerDuration);
} else {
$effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
$effectiveTrigger->add($triggerDuration);
}
}
} else {
$effectiveTrigger = $trigger->getDateTime();
}
return $effectiveTrigger;
}
/**
* 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 the CalDAV specification.
*
* @param \DateTime $start
* @param \DateTime $end
* @return bool
*/
public function isInTimeRange(\DateTime $start, \DateTime $end) {
$effectiveTrigger = $this->getEffectiveTriggerTime();
if (isset($this->DURATION)) {
$duration = VObject\DateTimeParser::parseDuration($this->DURATION);
$repeat = (string)$this->repeat;
if (!$repeat) {
$repeat = 1;
}
$period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat);
foreach($period as $occurrence) {
if ($start <= $occurrence && $end > $occurrence) {
return true;
}
}
return false;
} else {
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 array(
'ACTION' => 1,
'TRIGGER' => 1,
'DURATION' => '?',
'REPEAT' => '?',
'ATTACH' => '?',
);
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Sabre\VObject\Component;
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 {
/**
* 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
*/
function getValidationRules() {
return array(
'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
*/
function validate($options = 0) {
$result = parent::validate($options);
if (isset($this->DTEND) && isset($this->DURATION)) {
$result[] = array(
'level' => 3,
'message' => 'DTEND and DURATION cannot both be present',
'node' => $this
);
}
return $result;
}
}

View File

@@ -0,0 +1,526 @@
<?php
namespace Sabre\VObject\Component;
use DateTime;
use DateTimeZone;
use Sabre\VObject;
use Sabre\VObject\Component;
use Sabre\VObject\Recur\EventIterator;
use Sabre\VObject\Recur\NoInstancesException;
/**
* The VCalendar component
*
* This component adds functionality to a component, specific for a VCALENDAR.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VCalendar extends VObject\Document {
/**
* The default name for this component.
*
* This should be 'VCALENDAR' or 'VCARD'.
*
* @var string
*/
static $defaultName = 'VCALENDAR';
/**
* This is a list of components, and which classes they should map to.
*
* @var array
*/
static $componentMap = array(
'VALARM' => 'Sabre\\VObject\\Component\\VAlarm',
'VEVENT' => 'Sabre\\VObject\\Component\\VEvent',
'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy',
'VAVAILABILITY' => 'Sabre\\VObject\\Component\\VAvailability',
'AVAILABLE' => 'Sabre\\VObject\\Component\\Available',
'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal',
'VTIMEZONE' => 'Sabre\\VObject\\Component\\VTimeZone',
'VTODO' => 'Sabre\\VObject\\Component\\VTodo',
);
/**
* List of value-types, and which classes they map to.
*
* @var array
*/
static $valueMap = array(
'BINARY' => 'Sabre\\VObject\\Property\\Binary',
'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean',
'CAL-ADDRESS' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress',
'DATE' => 'Sabre\\VObject\\Property\\ICalendar\\Date',
'DATE-TIME' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration',
'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue',
'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue',
'PERIOD' => 'Sabre\\VObject\\Property\\ICalendar\\Period',
'RECUR' => 'Sabre\\VObject\\Property\\ICalendar\\Recur',
'TEXT' => 'Sabre\\VObject\\Property\\Text',
'TIME' => 'Sabre\\VObject\\Property\\Time',
'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only.
'URI' => 'Sabre\\VObject\\Property\\Uri',
'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset',
);
/**
* List of properties, and which classes they map to.
*
* @var array
*/
static $propertyMap = array(
// Calendar properties
'CALSCALE' => 'Sabre\\VObject\\Property\\FlatText',
'METHOD' => 'Sabre\\VObject\\Property\\FlatText',
'PRODID' => 'Sabre\\VObject\\Property\\FlatText',
'VERSION' => 'Sabre\\VObject\\Property\\FlatText',
// Component properties
'ATTACH' => 'Sabre\\VObject\\Property\\Uri',
'CATEGORIES' => 'Sabre\\VObject\\Property\\Text',
'CLASS' => 'Sabre\\VObject\\Property\\FlatText',
'COMMENT' => 'Sabre\\VObject\\Property\\FlatText',
'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText',
'GEO' => 'Sabre\\VObject\\Property\\FloatValue',
'LOCATION' => 'Sabre\\VObject\\Property\\FlatText',
'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\IntegerValue',
'PRIORITY' => 'Sabre\\VObject\\Property\\IntegerValue',
'RESOURCES' => 'Sabre\\VObject\\Property\\Text',
'STATUS' => 'Sabre\\VObject\\Property\\FlatText',
'SUMMARY' => 'Sabre\\VObject\\Property\\FlatText',
// Date and Time Component Properties
'COMPLETED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'DTEND' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'DUE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'DTSTART' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration',
'FREEBUSY' => 'Sabre\\VObject\\Property\\ICalendar\\Period',
'TRANSP' => 'Sabre\\VObject\\Property\\FlatText',
// Time Zone Component Properties
'TZID' => 'Sabre\\VObject\\Property\\FlatText',
'TZNAME' => 'Sabre\\VObject\\Property\\FlatText',
'TZOFFSETFROM' => 'Sabre\\VObject\\Property\\UtcOffset',
'TZOFFSETTO' => 'Sabre\\VObject\\Property\\UtcOffset',
'TZURL' => 'Sabre\\VObject\\Property\\Uri',
// Relationship Component Properties
'ATTENDEE' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress',
'CONTACT' => 'Sabre\\VObject\\Property\\FlatText',
'ORGANIZER' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress',
'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'RELATED-TO' => 'Sabre\\VObject\\Property\\FlatText',
'URL' => 'Sabre\\VObject\\Property\\Uri',
'UID' => 'Sabre\\VObject\\Property\\FlatText',
// Recurrence Component Properties
'EXDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'RDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'RRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur',
'EXRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', // Deprecated since rfc5545
// Alarm Component Properties
'ACTION' => 'Sabre\\VObject\\Property\\FlatText',
'REPEAT' => 'Sabre\\VObject\\Property\\IntegerValue',
'TRIGGER' => 'Sabre\\VObject\\Property\\ICalendar\\Duration',
// Change Management Component Properties
'CREATED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'DTSTAMP' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'SEQUENCE' => 'Sabre\\VObject\\Property\\IntegerValue',
// Request Status
'REQUEST-STATUS' => 'Sabre\\VObject\\Property\\Text',
// Additions from draft-daboo-valarm-extensions-04
'ALARM-AGENT' => 'Sabre\\VObject\\Property\\Text',
'ACKNOWLEDGED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
'PROXIMITY' => 'Sabre\\VObject\\Property\\Text',
'DEFAULT-ALARM' => 'Sabre\\VObject\\Property\\Boolean',
// Additions from draft-daboo-calendar-availability-05
'BUSYTYPE' => 'Sabre\\VObject\\Property\\Text',
);
/**
* Returns the current document type.
*
* @return void
*/
function getDocumentType() {
return self::ICALENDAR20;
}
/**
* Returns a list of all 'base components'. For instance, if an Event has
* a recurrence rule, and one instance is overridden, the overridden event
* will have the same UID, but will be excluded from this list.
*
* VTIMEZONE components will always be excluded.
*
* @param string $componentName filter by component name
* @return VObject\Component[]
*/
function getBaseComponents($componentName = null) {
$components = array();
foreach($this->children as $component) {
if (!$component instanceof VObject\Component)
continue;
if (isset($component->{'RECURRENCE-ID'}))
continue;
if ($componentName && $component->name !== strtoupper($componentName))
continue;
if ($component->name === 'VTIMEZONE')
continue;
$components[] = $component;
}
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
*/
function getBaseComponent($componentName = null) {
foreach($this->children as $component) {
if (!$component instanceof VObject\Component)
continue;
if (isset($component->{'RECURRENCE-ID'}))
continue;
if ($componentName && $component->name !== strtoupper($componentName))
continue;
if ($component->name === 'VTIMEZONE')
continue;
return $component;
}
}
/**
* If this calendar object, has events with recurrence rules, this method
* can be used to expand the event into multiple sub-events.
*
* Each event will be stripped from it's recurrence information, and only
* the instances of the event in the specified timerange will be left
* alone.
*
* In addition, this method will cause timezone information to be stripped,
* and normalized to UTC.
*
* This method will alter the VCalendar. This cannot be reversed.
*
* This functionality is specifically used by the CalDAV standard. It is
* 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
* @param DateTimeZone $timeZone reference timezone for floating dates and
* times.
* @return void
*/
function expand(DateTime $start, DateTime $end, DateTimeZone $timeZone = null) {
$newEvents = array();
if (!$timeZone) {
$timeZone = new DateTimeZone('UTC');
}
// An array of events. Events are indexed by UID. Each item in this
// array is a list of one or more events that match the UID.
$recurringEvents = array();
foreach($this->select('VEVENT') as $key=>$vevent) {
$uid = (string)$vevent->UID;
if (!$uid) {
throw new \LogicException('Event did not have a UID!');
}
if (isset($vevent->{'RECURRENCE-ID'}) || isset($vevent->RRULE)) {
if (isset($recurringEvents[$uid])) {
$recurringEvents[$uid][] = $vevent;
} else {
$recurringEvents[$uid] = array($vevent);
}
continue;
}
if (!isset($vevent->RRULE)) {
if ($vevent->isInTimeRange($start, $end)) {
$newEvents[] = $vevent;
}
continue;
}
}
foreach($recurringEvents as $events) {
try {
$it = new EventIterator($events, $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) {
$newEvents[] = $it->getEventObject();
}
$it->next();
}
}
// Wiping out all old VEVENT objects
unset($this->VEVENT);
// 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($timeZone);
// We only need to update the first timezone, because
// setDateTimes will match all other timezones to the
// first.
$dt[0]->setTimeZone(new DateTimeZone('UTC'));
$child->setDateTimes($dt);
}
}
$this->add($newEvent);
}
// Removing all VTIMEZONE components
unset($this->VTIMEZONE);
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults() {
return array(
'VERSION' => '2.0',
'PRODID' => '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN',
'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
*/
function getValidationRules() {
return array(
'PRODID' => 1,
'VERSION' => 1,
'CALSCALE' => '?',
'METHOD' => '?',
);
}
/**
* 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
*/
function validate($options = 0) {
$warnings = parent::validate($options);
if ($ver = $this->VERSION) {
if ((string)$ver !== '2.0') {
$warnings[] = array(
'level' => 3,
'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
'node' => $this,
);
}
}
$uidList = array();
$componentsFound = 0;
$componentTypes = array();
foreach($this->children as $child) {
if($child instanceof Component) {
$componentsFound++;
if (!in_array($child->name, array('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[] = array(
'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] = array(
'count' => 1,
'hasMaster' => $isMaster,
);
}
}
}
if ($componentsFound===0) {
$warnings[] = array(
'level' => 3,
'message' => 'An iCalendar object must have at least 1 component.',
'node' => $this,
);
}
if ($options & self::PROFILE_CALDAV) {
if (count($uidList)>1) {
$warnings[] = array(
'level' => 3,
'message' => 'A calendar object on a CalDAV server may only have components with the same UID.',
'node' => $this,
);
}
if (count(array_unique($componentTypes))===0) {
$warnings[] = array(
'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[] = array(
'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[] = array(
'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
*/
function getByUID($uid) {
return array_filter($this->children, function($item) use ($uid) {
if (!$item instanceof Component) {
return false;
}
if (!$itemUid = $item->select('UID')) {
return false;
}
$itemUid = current($itemUid)->getValue();
return $uid === $itemUid;
});
}
}

View File

@@ -0,0 +1,452 @@
<?php
namespace Sabre\VObject\Component;
use
Sabre\VObject;
/**
* The VCard component
*
* This component represents the BEGIN:VCARD and END:VCARD found in every
* vcard.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VCard extends VObject\Document {
/**
* The default name for this component.
*
* This should be 'VCALENDAR' or 'VCARD'.
*
* @var string
*/
static $defaultName = 'VCARD';
/**
* Caching the version number
*
* @var int
*/
private $version = null;
/**
* List of value-types, and which classes they map to.
*
* @var array
*/
static $valueMap = array(
'BINARY' => 'Sabre\\VObject\\Property\\Binary',
'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean',
'CONTENT-ID' => 'Sabre\\VObject\\Property\\FlatText', // vCard 2.1 only
'DATE' => 'Sabre\\VObject\\Property\\VCard\\Date',
'DATE-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateTime',
'DATE-AND-OR-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', // vCard only
'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue',
'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue',
'LANGUAGE-TAG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag',
'TIMESTAMP' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp',
'TEXT' => 'Sabre\\VObject\\Property\\Text',
'TIME' => 'Sabre\\VObject\\Property\\Time',
'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only.
'URI' => 'Sabre\\VObject\\Property\\Uri',
'URL' => 'Sabre\\VObject\\Property\\Uri', // vCard 2.1 only
'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset',
);
/**
* List of properties, and which classes they map to.
*
* @var array
*/
static $propertyMap = array(
// vCard 2.1 properties and up
'N' => 'Sabre\\VObject\\Property\\Text',
'FN' => 'Sabre\\VObject\\Property\\FlatText',
'PHOTO' => 'Sabre\\VObject\\Property\\Binary', // Todo: we should add a class for Binary values.
'BDAY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime',
'ADR' => 'Sabre\\VObject\\Property\\Text',
'LABEL' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0
'TEL' => 'Sabre\\VObject\\Property\\FlatText',
'EMAIL' => 'Sabre\\VObject\\Property\\FlatText',
'MAILER' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0
'GEO' => 'Sabre\\VObject\\Property\\FlatText',
'TITLE' => 'Sabre\\VObject\\Property\\FlatText',
'ROLE' => 'Sabre\\VObject\\Property\\FlatText',
'LOGO' => 'Sabre\\VObject\\Property\\Binary',
// 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so
// not supported at the moment
'ORG' => 'Sabre\\VObject\\Property\\Text',
'NOTE' => 'Sabre\\VObject\\Property\\FlatText',
'REV' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp',
'SOUND' => 'Sabre\\VObject\\Property\\FlatText',
'URL' => 'Sabre\\VObject\\Property\\Uri',
'UID' => 'Sabre\\VObject\\Property\\FlatText',
'VERSION' => 'Sabre\\VObject\\Property\\FlatText',
'KEY' => 'Sabre\\VObject\\Property\\FlatText',
'TZ' => 'Sabre\\VObject\\Property\\Text',
// vCard 3.0 properties
'CATEGORIES' => 'Sabre\\VObject\\Property\\Text',
'SORT-STRING' => 'Sabre\\VObject\\Property\\FlatText',
'PRODID' => 'Sabre\\VObject\\Property\\FlatText',
'NICKNAME' => 'Sabre\\VObject\\Property\\Text',
'CLASS' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0
// rfc2739 properties
'FBURL' => 'Sabre\\VObject\\Property\\Uri',
'CAPURI' => 'Sabre\\VObject\\Property\\Uri',
'CALURI' => 'Sabre\\VObject\\Property\\Uri',
// rfc4770 properties
'IMPP' => 'Sabre\\VObject\\Property\\Uri',
// vCard 4.0 properties
'XML' => 'Sabre\\VObject\\Property\\FlatText',
'ANNIVERSARY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime',
'CLIENTPIDMAP' => 'Sabre\\VObject\\Property\\Text',
'LANG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag',
'GENDER' => 'Sabre\\VObject\\Property\\Text',
'KIND' => 'Sabre\\VObject\\Property\\FlatText',
// rfc6474 properties
'BIRTHPLACE' => 'Sabre\\VObject\\Property\\FlatText',
'DEATHPLACE' => 'Sabre\\VObject\\Property\\FlatText',
'DEATHDATE' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime',
// rfc6715 properties
'EXPERTISE' => 'Sabre\\VObject\\Property\\FlatText',
'HOBBY' => 'Sabre\\VObject\\Property\\FlatText',
'INTEREST' => 'Sabre\\VObject\\Property\\FlatText',
'ORG-DIRECTORY' => 'Sabre\\VObject\\Property\\FlatText',
);
/**
* Returns the current document type.
*
* @return void
*/
function getDocumentType() {
if (!$this->version) {
$version = (string)$this->VERSION;
switch($version) {
case '2.1' :
$this->version = self::VCARD21;
break;
case '3.0' :
$this->version = self::VCARD30;
break;
case '4.0' :
$this->version = self::VCARD40;
break;
default :
$this->version = self::UNKNOWN;
break;
}
}
return $this->version;
}
/**
* Converts the document to a different vcard version.
*
* Use one of the VCARD constants for the target. This method will return
* a copy of the vcard in the new version.
*
* At the moment the only supported conversion is from 3.0 to 4.0.
*
* If input and output version are identical, a clone is returned.
*
* @param int $target
* @return VCard
*/
function convert($target) {
$converter = new VObject\VCardConverter();
return $converter->convert($this, $target);
}
/**
* VCards with version 2.1, 3.0 and 4.0 are found.
*
* If the VCARD doesn't know its version, 2.1 is assumed.
*/
const DEFAULT_VERSION = self::VCARD21;
/**
* 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
*/
function validate($options = 0) {
$warnings = array();
$versionMap = array(
self::VCARD21 => '2.1',
self::VCARD30 => '3.0',
self::VCARD40 => '4.0',
);
$version = $this->select('VERSION');
if (count($version)===1) {
$version = (string)$this->VERSION;
if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') {
$warnings[] = array(
'level' => 3,
'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
'node' => $this,
);
if ($options & self::REPAIR) {
$this->VERSION = $versionMap[self::DEFAULT_VERSION];
}
}
if ($version === '2.1' && ($options & self::PROFILE_CARDDAV)) {
$warnings[] = array(
'level' => 3,
'message' => 'CardDAV servers are not allowed to accept vCard 2.1.',
'node' => $this,
);
}
}
$uid = $this->select('UID');
if (count($uid) === 0) {
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[] = array(
'level' => $warningLevel,
'message' => $message,
'node' => $this,
);
}
$fn = $this->select('FN');
if (count($fn)!==1) {
$repaired = false;
if (($options & self::REPAIR) && count($fn) === 0) {
// We're going to try to see if we can use the contents of the
// N property.
if (isset($this->N)) {
$value = explode(';', (string)$this->N);
if (isset($value[1]) && $value[1]) {
$this->FN = $value[1] . ' ' . $value[0];
} else {
$this->FN = $value[0];
}
$repaired = true;
// Otherwise, the ORG property may work
} elseif (isset($this->ORG)) {
$this->FN = (string)$this->ORG;
$repaired = true;
}
}
$warnings[] = array(
'level' => $repaired?1:3,
'message' => 'The FN property must appear in the VCARD component exactly 1 time',
'node' => $this,
);
}
return array_merge(
parent::validate($options),
$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
*/
function getValidationRules() {
return array(
'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.
*
* VCards can indicate wether a field such as ADR, TEL or EMAIL is
* preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x
* being a number between 1 and 100).
*
* If neither of those parameters are specified, the first is returned, if
* a field with that name does not exist, null is returned.
*
* @param string $fieldName
* @return VObject\Property|null
*/
function preferred($propertyName) {
$preferred = null;
$lastPref = 101;
foreach($this->select($propertyName) as $field) {
$pref = 101;
if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) {
$pref = 1;
} elseif (isset($field['PREF'])) {
$pref = $field['PREF']->getValue();
}
if ($pref < $lastPref || is_null($preferred)) {
$preferred = $field;
$lastPref = $pref;
}
}
return $preferred;
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults() {
return array(
'VERSION' => '3.0',
'PRODID' => '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN',
);
}
/**
* This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents.
*
* @return array
*/
function jsonSerialize() {
// A vcard does not have sub-components, so we're overriding this
// method to remove that array element.
$properties = array();
foreach($this->children as $child) {
$properties[] = $child->jsonSerialize();
}
return array(
strtolower($this->name),
$properties,
);
}
/**
* Returns the default class for a property name.
*
* @param string $propertyName
* @return string
*/
function getClassNameForPropertyName($propertyName) {
$className = parent::getClassNameForPropertyName($propertyName);
// In vCard 4, BINARY no longer exists, and we need URI instead.
if ($className == 'Sabre\\VObject\\Property\\Binary' && $this->getDocumentType()===self::VCARD40) {
return 'Sabre\\VObject\\Property\\Uri';
}
return $className;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
use Sabre\VObject\Recur\EventIterator;
use Sabre\VObject\Recur\NoInstancesException;
/**
* VEvent component
*
* This component contains some additional functionality specific for VEVENT's.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VEvent 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 the CalDAV specification.
*
* @param \DateTime $start
* @param \DateTime $end
* @return bool
*/
public function isInTimeRange(\DateTime $start, \DateTime $end) {
if ($this->RRULE) {
try {
$it = new EventIterator($this, null, $start->getTimezone());
} catch (NoInstancesException $e) {
// If we've catched this exception, there are no instances
// for the event that fall into the specified time-range.
return false;
}
$it->fastForward($start);
// We fast-forwarded to a spot where the end-time of the
// recurrence instance exceeded the start of the requested
// time-range.
//
// If the starttime of the recurrence did not exceed the
// end of the time range as well, we have a match.
return ($it->getDTStart() < $end && $it->getDTEnd() > $start);
}
$effectiveStart = $this->DTSTART->getDateTime($start->getTimezone());
if (isset($this->DTEND)) {
// 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
// July 4th respectively.
//
// See:
// http://tools.ietf.org/html/rfc5545#page-54
$effectiveEnd = $this->DTEND->getDateTime($end->getTimezone());
} elseif (isset($this->DURATION)) {
$effectiveEnd = clone $effectiveStart;
$effectiveEnd->add(VObject\DateTimeParser::parseDuration($this->DURATION));
} elseif (!$this->DTSTART->hasTime()) {
$effectiveEnd = clone $effectiveStart;
$effectiveEnd->modify('+1 day');
} else {
$effectiveEnd = clone $effectiveStart;
}
return (
($start < $effectiveEnd) && ($end > $effectiveStart)
);
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults() {
return array(
'UID' => 'sabre-vobject-' . VObject\UUIDUtil::getUUID(),
'DTSTAMP' => date('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 array(
'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

@@ -0,0 +1,103 @@
<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
/**
* The VFreeBusy component
*
* This component adds functionality to a component, specific for VFREEBUSY
* components.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VFreeBusy extends VObject\Component {
/**
* Checks based on the contained FREEBUSY information, if a timeslot is
* available.
*
* @param DateTime $start
* @param Datetime $end
* @return bool
*/
public function isFree(\DateTime $start, \Datetime $end) {
foreach($this->select('FREEBUSY') as $freebusy) {
// We are only interested in FBTYPE=BUSY (the default),
// FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') {
continue;
}
// The freebusy component can hold more than 1 value, separated by
// commas.
$periods = explode(',', (string)$freebusy);
foreach($periods as $period) {
// Every period is formatted as [start]/[end]. The start is an
// absolute UTC time, the end may be an absolute UTC time, or
// duration (relative) value.
list($busyStart, $busyEnd) = explode('/', $period);
$busyStart = VObject\DateTimeParser::parse($busyStart);
$busyEnd = VObject\DateTimeParser::parse($busyEnd);
if ($busyEnd instanceof \DateInterval) {
$tmp = clone $busyStart;
$tmp->add($busyEnd);
$busyEnd = $tmp;
}
if($start < $busyEnd && $end > $busyStart) {
return false;
}
}
}
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 array(
'UID' => 1,
'DTSTAMP' => 1,
'CONTACT' => '?',
'DTSTART' => '?',
'DTEND' => '?',
'ORGANIZER' => '?',
'URL' => '?',
'ATTENDEE' => '*',
'COMMENT' => '*',
'FREEBUSY' => '*',
'REQUEST-STATUS' => '*',
);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
/**
* VJournal component
*
* This component contains some additional functionality specific for VJOURNALs.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VJournal 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 the CalDAV specification.
*
* @param DateTime $start
* @param DateTime $end
* @return bool
*/
public function isInTimeRange(\DateTime $start, \DateTime $end) {
$dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
if ($dtstart) {
$effectiveEnd = clone $dtstart;
if (!$this->DTSTART->hasTime()) {
$effectiveEnd->modify('+1 day');
}
return ($start <= $effectiveEnd && $end > $dtstart);
}
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 array(
'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' => '*',
);
}
}

View File

@@ -0,0 +1,68 @@
<?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
*/
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
*/
function getValidationRules() {
return array(
'TZID' => 1,
'LAST-MODIFIED' => '?',
'TZURL' => '?',
// At least 1 STANDARD or DAYLIGHT must appear, or more. But both
// cannot appear in the same VTIMEZONE.
//
// The validator is not specific yet to pick this up, so these
// rules are too loose.
'STANDARD' => '*',
'DAYLIGHT' => '*',
);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
/**
* VTodo component
*
* This component contains some additional functionality specific for VTODOs.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VTodo 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 the CalDAV specification.
*
* @param DateTime $start
* @param DateTime $end
* @return bool
*/
public function isInTimeRange(\DateTime $start, \DateTime $end) {
$dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
$duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null;
$due = isset($this->DUE)?$this->DUE->getDateTime():null;
$completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null;
$created = isset($this->CREATED)?$this->CREATED->getDateTime():null;
if ($dtstart) {
if ($duration) {
$effectiveEnd = clone $dtstart;
$effectiveEnd->add($duration);
return $start <= $effectiveEnd && $end > $dtstart;
} elseif ($due) {
return
($start < $due || $start <= $dtstart) &&
($end > $dtstart || $end >= $due);
} else {
return $start <= $dtstart && $end > $dtstart;
}
}
if ($due) {
return ($start < $due && $end >= $due);
}
if ($completed && $created) {
return
($start <= $created || $start <= $completed) &&
($end >= $created || $end >= $completed);
}
if ($completed) {
return ($start <= $completed && $end >= $completed);
}
if ($created) {
return ($end > $created);
}
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 array(
'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[] = array(
'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[] = array(
'level' => 3,
'message' => 'DUE must occur after DTSTART',
'node' => $due,
);
}
}
return $result;
}
}

View File

@@ -0,0 +1,431 @@
<?php
namespace Sabre\VObject;
use DateTime;
use DateTimeZone;
use DateInterval;
use InvalidArgumentException;
use LogicException;
/**
* DateTimeParser
*
* This class is responsible for parsing the several different date and time
* formats iCalendar and vCards have.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class DateTimeParser {
/**
* Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object
*
* 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
* returned DateTime object will still be in the UTC timezone.
*
* @param string $dt
* @param DateTimeZone $tz
* @return DateTime
*/
static public function parseDateTime($dt, DateTimeZone $tz = null) {
// 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);
if (!$result) {
throw new LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt);
}
if ($matches[7]==='Z' || is_null($tz)) {
$tz = new DateTimeZone('UTC');
}
$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;
}
/**
* Parses an iCalendar (rfc5545) formatted date and returns a DateTime object.
*
* @param string $date
* @param DateTimeZone $tz
* @return DateTime
*/
static public function parseDate($date, DateTimeZone $tz = null) {
// Format is YYYYMMDD
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
if (!$result) {
throw new LogicException('The supplied iCalendar date value is incorrect: ' . $date);
}
if (is_null($tz)) {
$tz = new DateTimeZone('UTC');
}
$date = new DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], $tz);
return $date;
}
/**
* Parses an iCalendar (RFC5545) formatted duration value.
*
* This method will either return a DateTimeInterval object, or a string
* suitable for strtotime or DateTime::modify.
*
* @param string $duration
* @param bool $asString
* @return DateInterval|string
*/
static public 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);
if (!$result) {
throw new LogicException('The supplied iCalendar duration value is incorrect: ' . $duration);
}
if (!$asString) {
$invert = false;
if ($matches['plusminus']==='-') {
$invert = true;
}
$parts = array(
'week',
'day',
'hour',
'minute',
'second',
);
foreach($parts as $part) {
$matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0;
}
// We need to re-construct the $duration string, because weeks and
// days are not supported by DateInterval in the same string.
$duration = 'P';
$days = $matches['day'];
if ($matches['week']) {
$days+=$matches['week']*7;
}
if ($days)
$duration.=$days . 'D';
if ($matches['minute'] || $matches['second'] || $matches['hour']) {
$duration.='T';
if ($matches['hour'])
$duration.=$matches['hour'].'H';
if ($matches['minute'])
$duration.=$matches['minute'].'M';
if ($matches['second'])
$duration.=$matches['second'].'S';
}
if ($duration==='P') {
$duration = 'PT0S';
}
$iv = new DateInterval($duration);
if ($invert) $iv->invert = true;
return $iv;
}
$parts = array(
'week',
'day',
'hour',
'minute',
'second',
);
$newDur = '';
foreach($parts as $part) {
if (isset($matches[$part]) && $matches[$part]) {
$newDur.=' '.$matches[$part] . ' ' . $part . 's';
}
}
$newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur);
if ($newDur === '+') {
$newDur = '+0 seconds';
};
return $newDur;
}
/**
* Parses either a Date or DateTime, or Duration value.
*
* @param string $date
* @param DateTimeZone|string $referenceTz
* @return DateTime|DateInterval
*/
static public function parse($date, $referenceTz = null) {
if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) {
return self::parseDuration($date);
} elseif (strlen($date)===8) {
return self::parseDate($date, $referenceTz);
} else {
return self::parseDateTime($date, $referenceTz);
}
}
/**
* This method parses a vCard date and or time value.
*
* This can be used for the DATE, DATE-TIME, TIMESTAMP 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 '+08:00'
*
* For any non-specified values null is returned.
*
* List of date formats that are supported:
* YYYY
* YYYY-MM
* YYYYMMDD
* --MMDD
* ---DD
*
* YYYY-MM-DD
* --MM-DD
* ---DD
*
* List of supported time formats:
*
* HH
* HHMM
* HHMMSS
* -MMSS
* --SS
*
* HH
* HH:MM
* HH:MM:SS
* -MM:SS
* --SS
*
* A full basic-format date-time string looks like :
* 20130603T133901
*
* A full extended-format date-time string looks like :
* 2013-06-03T13:39:01
*
* 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
*/
static public function parseVCardDateTime($date) {
$regex = '/^
(?: # date part
(?:
(?: (?P<year> [0-9]{4}) (?: -)?| --)
(?P<month> [0-9]{2})?
|---)
(?P<date> [0-9]{2})?
)?
(?:T # time part
(?P<hour> [0-9]{2} | -)
(?P<minute> [0-9]{2} | -)?
(?P<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{4})
)?
)?
$/x';
if (!preg_match($regex, $date, $matches)) {
// Attempting to parse the extended format.
$regex = '/^
(?: # date part
(?: (?P<year> [0-9]{4}) - | -- )
(?P<month> [0-9]{2}) -
(?P<date> [0-9]{2})
)?
(?:T # time part
(?: (?P<hour> [0-9]{2}) : | -)
(?: (?P<minute> [0-9]{2}) : | -)?
(?P<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
)?
)?
$/x';
if (!preg_match($regex, $date, $matches)) {
throw new InvalidArgumentException('Invalid vCard date-time string: ' . $date);
}
}
$parts = array(
'year',
'month',
'date',
'hour',
'minute',
'second',
'timezone'
);
$result = array();
foreach($parts as $part) {
if (empty($matches[$part])) {
$result[$part] = null;
} elseif ($matches[$part] === '-' || $matches[$part] === '--') {
$result[$part] = null;
} else {
$result[$part] = $matches[$part];
}
}
return $result;
}
/**
* This method parses a vCard TIME value.
*
* This method returns an array, not a DateTime value.
*
* The elements in the array are in the following order:
* 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 hour etc.
*
* Timezone is either returned as 'Z' or as '+08:00'
*
* For any non-specified values null is returned.
*
* List of supported time formats:
*
* HH
* HHMM
* HHMMSS
* -MMSS
* --SS
*
* HH
* HH:MM
* HH:MM:SS
* -MM:SS
* --SS
*
* A full basic-format time string looks like :
* 133901
*
* A full extended-format time string looks like :
* 13:39:01
*
* Times may be postfixed by a timezone offset. This can be either 'Z' for
* UTC, or a string like -0500 or +11:00.
*
* @param string $date
* @return array
*/
static public function parseVCardTime($date) {
$regex = '/^
(?P<hour> [0-9]{2} | -)
(?P<minute> [0-9]{2} | -)?
(?P<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{4})
)?
$/x';
if (!preg_match($regex, $date, $matches)) {
// Attempting to parse the extended format.
$regex = '/^
(?: (?P<hour> [0-9]{2}) : | -)
(?: (?P<minute> [0-9]{2}) : | -)?
(?P<second> [0-9]{2})?
(?: \.[0-9]{3})? # milliseconds
(?P<timezone> # timezone offset
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
)?
$/x';
if (!preg_match($regex, $date, $matches)) {
throw new InvalidArgumentException('Invalid vCard time string: ' . $date);
}
}
$parts = array(
'hour',
'minute',
'second',
'timezone'
);
$result = array();
foreach($parts as $part) {
if (empty($matches[$part])) {
$result[$part] = null;
} elseif ($matches[$part] === '-') {
$result[$part] = null;
} else {
$result[$part] = $matches[$part];
}
}
return $result;
}
}

261
vendor/sabre/vobject/lib/Document.php vendored Normal file
View File

@@ -0,0 +1,261 @@
<?php
namespace Sabre\VObject;
/**
* Document
*
* A document is just like a component, except that it's also the top level
* element.
*
* Both a VCALENDAR and a VCARD are considered documents.
*
* This class also provides a registry for document types.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class Document extends Component {
/**
* Unknown document type
*/
const UNKNOWN = 1;
/**
* vCalendar 1.0
*/
const VCALENDAR10 = 2;
/**
* iCalendar 2.0
*/
const ICALENDAR20 = 3;
/**
* vCard 2.1
*/
const VCARD21 = 4;
/**
* vCard 3.0
*/
const VCARD30 = 5;
/**
* vCard 4.0
*/
const VCARD40 = 6;
/**
* The default name for this component.
*
* This should be 'VCALENDAR' or 'VCARD'.
*
* @var string
*/
static public $defaultName;
/**
* List of properties, and which classes they map to.
*
* @var array
*/
static public $propertyMap = array();
/**
* List of components, along with which classes they map to.
*
* @var array
*/
static public $componentMap = array();
/**
* List of value-types, and which classes they map to.
*
* @var array
*/
static public $valueMap = array();
/**
* Creates a new document.
*
* We're changing the default behavior slightly here. First, we don't want
* to have to specify a name (we already know it), and we want to allow
* children to be specified in the first argument.
*
* But, the default behavior also works.
*
* So the two sigs:
*
* new Document(array $children = array(), $defaults = true);
* new Document(string $name, array $children = array(), $defaults = true)
*
* @return void
*/
public function __construct() {
$args = func_get_args();
if (count($args)===0 || is_array($args[0])) {
array_unshift($args, $this, static::$defaultName);
call_user_func_array(array('parent', '__construct'), $args);
} else {
array_unshift($args, $this);
call_user_func_array(array('parent', '__construct'), $args);
}
}
/**
* Returns the current document type.
*
* @return void
*/
public function getDocumentType() {
return self::UNKNOWN;
}
/**
* Creates a new component or property.
*
* If it's a known component, we will automatically call createComponent.
* otherwise, we'll assume it's a property and call createProperty instead.
*
* @param string $name
* @param string $arg1,... Unlimited number of args
* @return mixed
*/
public function create($name) {
if (isset(static::$componentMap[strtoupper($name)])) {
return call_user_func_array(array($this,'createComponent'), func_get_args());
} else {
return call_user_func_array(array($this,'createProperty'), func_get_args());
}
}
/**
* Creates a new component
*
* This method automatically searches for the correct component class, based
* on its name.
*
* You can specify the children either in key=>value syntax, in which case
* properties will automatically be created, or you can just pass a list of
* Component and Property object.
*
* By default, a set of sensible values will be added to the component. For
* an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
* ensure that this does not happen, set $defaults to false.
*
* @param string $name
* @param array $children
* @param bool $defaults
* @return Component
*/
public function createComponent($name, array $children = null, $defaults = true) {
$name = strtoupper($name);
$class = 'Sabre\\VObject\\Component';
if (isset(static::$componentMap[$name])) {
$class=static::$componentMap[$name];
}
if (is_null($children)) $children = array();
return new $class($this, $name, $children, $defaults);
}
/**
* Factory method for creating new properties
*
* This method automatically searches for the correct property class, based
* on its name.
*
* You can specify the parameters either in key=>value syntax, in which case
* parameters will automatically be created, or you can just pass a list of
* Parameter objects.
*
* @param string $name
* @param mixed $value
* @param array $parameters
* @param string $valueType Force a specific valuetype, such as URI or TEXT
* @return Property
*/
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 (($i=strpos($name,'.'))!==false) {
$group = substr($name, 0, $i);
$name = strtoupper(substr($name, $i+1));
} else {
$name = strtoupper($name);
$group = null;
}
$class = null;
if ($valueType) {
// The valueType argument comes first to figure out the correct
// class.
$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)) {
$class = $this->getClassNameForPropertyName($name);
}
if (is_null($parameters)) $parameters = array();
return new $class($this, $name, $value, $parameters, $group);
}
/**
* This method returns a full class-name for a value parameter.
*
* For instance, DTSTART may have VALUE=DATE. In that case we will look in
* our valueMap table and return the appropriate class name.
*
* This method returns null if we don't have a specialized class.
*
* @param string $valueParam
* @return void
*/
public function getClassNameForPropertyValue($valueParam) {
$valueParam = strtoupper($valueParam);
if (isset(static::$valueMap[$valueParam])) {
return static::$valueMap[$valueParam];
}
}
/**
* Returns the default class for a property name.
*
* @param string $propertyName
* @return string
*/
public function getClassNameForPropertyName($propertyName) {
if (isset(static::$propertyMap[$propertyName])) {
return static::$propertyMap[$propertyName];
} else {
return 'Sabre\\VObject\\Property\\Unknown';
}
}
}

172
vendor/sabre/vobject/lib/ElementList.php vendored Normal file
View File

@@ -0,0 +1,172 @@
<?php
namespace Sabre\VObject;
/**
* VObject ElementList
*
* This class represents a list of elements. Lists are the result of queries,
* such as doing $vcalendar->vevent where there's multiple VEVENT objects.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ElementList implements \Iterator, \Countable, \ArrayAccess {
/**
* 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 */
/**
* 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.
*
* @param int $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value) {
throw new \LogicException('You can not add new objects to an ElementList');
}
/**
* Sets an item through ArrayAccess.
*
* This method just forwards the request to the inner iterator
*
* @param int $offset
* @return void
*/
public function offsetUnset($offset) {
throw new \LogicException('You can not remove objects from an ElementList');
}
/* }}} */
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Sabre\VObject;
/**
* Exception thrown by parser when the end of the stream has been reached,
* before this was expected.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class EofException extends ParseException {
}

View File

@@ -0,0 +1,363 @@
<?php
namespace Sabre\VObject;
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
* objects.
*
* It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
* generates a single VFREEBUSY object.
*
* VFREEBUSY components are described in RFC5545, The rules for what should
* go in a single freebusy report is taken from RFC4791, section 7.10.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class FreeBusyGenerator {
/**
* Input objects
*
* @var array
*/
protected $objects;
/**
* Start of range
*
* @var DateTime|null
*/
protected $start;
/**
* End of range
*
* @var DateTime|null
*/
protected $end;
/**
* VCALENDAR object
*
* @var Component
*/
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;
/**
* Creates the generator.
*
* Check the setTimeRange and setObjects methods for details about the
* arguments.
*
* @param DateTime $start
* @param DateTime $end
* @param mixed $objects
* @param DateTimeZone $timeZone
* @return void
*/
public function __construct(\DateTime $start = null, \DateTime $end = null, $objects = null, DateTimeZone $timeZone = null) {
if ($start && $end) {
$this->setTimeRange($start, $end);
}
if ($objects) {
$this->setObjects($objects);
}
if (is_null($timeZone)) {
$timeZone = new DateTimeZone('UTC');
}
$this->setTimeZone($timeZone);
}
/**
* Sets the VCALENDAR object.
*
* If this is set, it will not be generated for you. You are responsible
* for setting things like the METHOD, CALSCALE, VERSION, etc..
*
* The VFREEBUSY object will be automatically added though.
*
* @param Component $vcalendar
* @return void
*/
public function setBaseObject(Component $vcalendar) {
$this->baseObject = $vcalendar;
}
/**
* Sets the input objects
*
* You must either specify a valendar object as a strong, or as the parse
* Component.
* It's also possible to specify multiple objects as an array.
*
* @param mixed $objects
* @return void
*/
public function setObjects($objects) {
if (!is_array($objects)) {
$objects = array($objects);
}
$this->objects = array();
foreach($objects as $object) {
if (is_string($object)) {
$this->objects[] = Reader::read($object);
} elseif ($object instanceof Component) {
$this->objects[] = $object;
} else {
throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects');
}
}
}
/**
* Sets the time range
*
* Any freebusy object falling outside of this time range will be ignored.
*
* @param DateTime $start
* @param DateTime $end
* @return void
*/
public function setTimeRange(\DateTime $start = null, \DateTime $end = null) {
$this->start = $start;
$this->end = $end;
}
/**
* Sets the reference timezone for floating times.
*
* @param DateTimeZone $timeZone
* @return void
*/
public function setTimeZone(DateTimeZone $timeZone) {
$this->timeZone = $timeZone;
}
/**
* Parses the input data and returns a correct VFREEBUSY object, wrapped in
* a VCALENDAR.
*
* @return Component
*/
public function getResult() {
$busyTimes = array();
foreach($this->objects as $key=>$object) {
foreach($object->getBaseComponents() as $component) {
switch($component->name) {
case 'VEVENT' :
$FBTYPE = 'BUSY';
if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) {
break;
}
if (isset($component->STATUS)) {
$status = strtoupper($component->STATUS);
if ($status==='CANCELLED') {
break;
}
if ($status==='TENTATIVE') {
$FBTYPE = 'BUSY-TENTATIVE';
}
}
$times = array();
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]);
continue;
}
if ($this->start) {
$iterator->fastForward($this->start);
}
$maxRecurrences = 200;
while($iterator->valid() && --$maxRecurrences) {
$startTime = $iterator->getDTStart();
if ($this->end && $startTime > $this->end) {
break;
}
$times[] = array(
$iterator->getDTStart(),
$iterator->getDTEnd(),
);
$iterator->next();
}
} else {
$startTime = $component->DTSTART->getDateTime($this->timeZone);
if ($this->end && $startTime > $this->end) {
break;
}
$endTime = null;
if (isset($component->DTEND)) {
$endTime = $component->DTEND->getDateTime($this->timeZone);
} elseif (isset($component->DURATION)) {
$duration = DateTimeParser::parseDuration((string)$component->DURATION);
$endTime = clone $startTime;
$endTime->add($duration);
} elseif (!$component->DTSTART->hasTime()) {
$endTime = clone $startTime;
$endTime->modify('+1 day');
} else {
// The event had no duration (0 seconds)
break;
}
$times[] = array($startTime, $endTime);
}
foreach($times as $time) {
if ($this->end && $time[0] > $this->end) break;
if ($this->start && $time[1] < $this->start) break;
$busyTimes[] = array(
$time[0],
$time[1],
$FBTYPE,
);
}
break;
case 'VFREEBUSY' :
foreach($component->FREEBUSY as $freebusy) {
$fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY';
// Skipping intervals marked as 'free'
if ($fbType==='FREE')
continue;
$values = explode(',', $freebusy);
foreach($values as $value) {
list($startTime, $endTime) = explode('/', $value);
$startTime = DateTimeParser::parseDateTime($startTime);
if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') {
$duration = DateTimeParser::parseDuration($endTime);
$endTime = clone $startTime;
$endTime->add($duration);
} else {
$endTime = DateTimeParser::parseDateTime($endTime);
}
if($this->start && $this->start > $endTime) continue;
if($this->end && $this->end < $startTime) continue;
$busyTimes[] = array(
$startTime,
$endTime,
$fbType
);
}
}
break;
}
}
}
if ($this->baseObject) {
$calendar = $this->baseObject;
} else {
$calendar = new VCalendar();
}
$vfreebusy = $calendar->createComponent('VFREEBUSY');
$calendar->add($vfreebusy);
if ($this->start) {
$dtstart = $calendar->createProperty('DTSTART');
$dtstart->setDateTime($this->start);
$vfreebusy->add($dtstart);
}
if ($this->end) {
$dtend = $calendar->createProperty('DTEND');
$dtend->setDateTime($this->end);
$vfreebusy->add($dtend);
}
$dtstamp = $calendar->createProperty('DTSTAMP');
$dtstamp->setDateTime(new \DateTime('now', new \DateTimeZone('UTC')));
$vfreebusy->add($dtstamp);
foreach($busyTimes as $busyTime) {
$busyTime[0]->setTimeZone(new \DateTimeZone('UTC'));
$busyTime[1]->setTimeZone(new \DateTimeZone('UTC'));
$prop = $calendar->createProperty(
'FREEBUSY',
$busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z')
);
$prop['FBTYPE'] = $busyTime[2];
$vfreebusy->add($prop);
}
return $calendar;
}
}

981
vendor/sabre/vobject/lib/ITip/Broker.php vendored Normal file
View File

@@ -0,0 +1,981 @@
<?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 = array(
'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 Message $itipMessage
* @param VCalendar $existingObject
* @return VCalendar|null
*/
public function processMessage(Message $itipMessage, VCalendar $existingObject = null) {
// We only support events at the moment.
if ($itipMessage->component !== 'VEVENT') {
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 null;
}
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 = null, $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 array();
}
$oldEventInfo = $this->parseEventInfo($oldCalendar);
} else {
$oldEventInfo = array(
'organizer' => null,
'significantChangeHash' => '',
'attendees' => array(),
);
}
$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 array();
}
$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 array();
}
if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) {
// There was no organizer before or after the change.
return array();
}
$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 array();
}
$eventInfo = $oldEventInfo;
if (in_array($eventInfo['organizer'], $userHref)) {
// This is an organizer deleting the event.
$eventInfo['attendees'] = array();
// 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'] = array('master' =>
array('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 array();
}
/**
* 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 Message $itipMessage
* @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 Message $itipMessage
* @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 Message $itipMessage
* @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 null;
}
$instances = array();
$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 ($recurId==='master') {
$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 attende 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, array(
'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 null;
}
// 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, array(
'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.
*
* @param VCalendar $calendar
* @param array $eventInfo
* @param array $oldEventInfo
* @return array
*/
protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) {
// Merging attendee lists.
$attendees = array();
foreach($oldEventInfo['attendees'] as $attendee) {
$attendees[$attendee['href']] = array(
'href' => $attendee['href'],
'oldInstances' => $attendee['instances'],
'newInstances' => array(),
'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']] = array(
'href' => $attendee['href'],
'oldInstances' => array(),
'newInstances' => $attendee['instances'],
'name' => $attendee['name'],
'forceSend' => $attendee['forceSend'],
);
}
}
$messages = array();
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'];
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';
// Creating the new iCalendar body.
$icalMsg = new VCalendar();
$icalMsg->METHOD = $message->method;
$event = $icalMsg->add('VEVENT', array(
'UID' => $message->uid,
'SEQUENCE' => $message->sequence,
));
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'], array(
'CN' => $attendee['name'],
));
$message->significantChange = true;
} else {
// The attendee gets the updated event body
$message->method = 'REQUEST';
// Creating the new iCalendar body.
$icalMsg = new VCalendar();
$icalMsg->METHOD = $message->method;
foreach($calendar->select('VTIMEZONE') as $timezone) {
$icalMsg->add(clone $timezone);
}
// 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.
$message->significantChange =
$attendee['forceSend'] === 'REQUEST' ||
array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) ||
$oldEventInfo['significantChangeHash']!==$eventInfo['significantChangeHash'];
foreach($attendee['newInstances'] as $instanceId => $instanceInfo) {
$currentEvent = clone $eventInfo['instances'][$instanceId];
if ($instanceId === 'master') {
// We need to find a list of events that the attendee
// is not a part of to add to the list of exceptions.
$exceptions = array();
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';
}
}
}
$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 VCalendar $calendar
* @param array $eventInfo
* @param array $oldEventInfo
* @param string $attendee
* @return Message[]
*/
protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) {
if ($this->scheduleAgentServerRules && $eventInfo['organizerScheduleAgent']==='CLIENT') {
return array();
}
// Don't bother generating messages for events that have already been
// cancelled.
if ($eventInfo['status']==='CANCELLED') {
return array();
}
$oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ?
$oldEventInfo['attendees'][$attendee]['instances'] :
array();
$instances = array();
foreach($oldInstances as $instance) {
$instances[$instance['id']] = array(
'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']] = array(
'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']) && $instances['master']['newstatus'] !== 'DECLINED') {
foreach($eventInfo['exdate'] as $exDate) {
if (!in_array($exDate, $oldEventInfo['exdate'])) {
if (isset($instances[$exDate])) {
$instances[$exDate]['newstatus'] = 'DECLINED';
} else {
$instances[$exDate] = array(
'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';
$hasReply = false;
foreach($instances as $instance) {
if ($instance['oldstatus']==$instance['newstatus'] && $eventInfo['organizerForceSend'] !== 'REPLY') {
// Skip
continue;
}
$event = $icalMsg->add('VEVENT', array(
'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) {
$recur = $event->add('DTSTART', $dt, array('VALUE' => 'DATE'));
} else {
$recur = $event->add('DTSTART', $dt);
}
if ($summary) {
$event->add('SUMMARY', $summary);
}
}
if ($instance['id'] !== 'master') {
$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
// Treat is as a DATE field
if (strlen($instance['id']) <= 8) {
$recur = $event->add('RECURRENCE-ID', $dt, array('VALUE' => 'DATE'));
} else {
$recur = $event->add('RECURRENCE-ID', $dt);
}
}
$organizer = $event->add('ORGANIZER', $message->recipient);
if ($message->recipientName) {
$organizer['CN'] = $message->recipientName;
}
$attendee = $event->add('ATTENDEE', $message->sender, array(
'PARTSTAT' => $instance['newstatus']
));
if ($message->senderName) {
$attendee['CN'] = $message->senderName;
}
$hasReply = true;
}
if ($hasReply) {
$message->message = $icalMsg;
return array($message);
} else {
return array();
}
}
/**
* 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 = array();
$instances = array();
$exdate = array();
foreach($calendar->VEVENT as $vevent) {
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 ($organizer !== $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->STATUS)) {
$status = strtoupper($vevent->STATUS->getValue());
}
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
if (is_null($timezone)) {
if ($recurId === 'master') {
$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']) &&
strtoupper($attendee['SCHEDULE-AGENT']->getValue()) === 'CLIENT'
) {
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] = array(
'id' => $recurId,
'partstat' => $partStat,
'force-send' => $forceSend,
);
} else {
$attendees[$attendee->getNormalizedValue()] = array(
'href' => $attendee->getNormalizedValue(),
'instances' => array(
$recurId => array(
'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);
$significantChangeHash.=$prop.':';
if ($prop === 'EXDATE') {
$significantChangeHash.= implode(',', $exdate).';';
} else {
foreach($propertyValues as $val) {
$significantChangeHash.= $val->getValue().';';
}
}
}
}
}
$significantChangeHash = md5($significantChangeHash);
return compact(
'uid',
'organizer',
'organizerName',
'organizerScheduleAgent',
'organizerForceSend',
'instances',
'attendees',
'sequence',
'exdate',
'timezone',
'significantChangeHash',
'status'
);
}
}

View File

@@ -0,0 +1,15 @@
<?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,141 @@
<?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 {
}

226
vendor/sabre/vobject/lib/Node.php vendored Normal file
View File

@@ -0,0 +1,226 @@
<?php
namespace Sabre\VObject;
/**
* A node is the root class for every element in an iCalendar of vCard object.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
/**
* 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;
/**
* 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.
*
* @var Node
*/
public $parent;
/**
* Iterator override
*
* @var ElementList
*/
protected $iterator = null;
/**
* The root document
*
* @var Component
*/
protected $root;
/**
* Serializes the node into a mimedir format
*
* @return string
*/
abstract public function serialize();
/**
* This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents.
*
* @return array
*/
abstract public function jsonSerialize();
/* {{{ IteratorAggregator interface */
/**
* Returns the iterator for this object
*
* @return ElementList
*/
public function getIterator() {
if (!is_null($this->iterator))
return $this->iterator;
return new ElementList(array($this));
}
/**
* Sets the overridden iterator
*
* Note that this is not actually part of the iterator interface
*
* @param ElementList $iterator
* @return void
*/
public function setIterator(ElementList $iterator) {
$this->iterator = $iterator;
}
/**
* 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) {
return array();
}
/* }}} */
/* {{{ Countable interface */
/**
* Returns the number of elements
*
* @return int
*/
public function count() {
$it = $this->getIterator();
return $it->count();
}
/* }}} */
/* {{{ ArrayAccess Interface */
/**
* Checks if an item exists through ArrayAccess.
*
* This method just forwards the request to the inner iterator
*
* @param int $offset
* @return bool
*/
public function offsetExists($offset) {
$iterator = $this->getIterator();
return $iterator->offsetExists($offset);
}
/**
* Gets an item through ArrayAccess.
*
* This method just forwards the request to the inner iterator
*
* @param int $offset
* @return mixed
*/
public function offsetGet($offset) {
$iterator = $this->getIterator();
return $iterator->offsetGet($offset);
}
/**
* Sets an item through ArrayAccess.
*
* This method just forwards the request to the inner iterator
*
* @param int $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value) {
$iterator = $this->getIterator();
$iterator->offsetSet($offset,$value);
// @codeCoverageIgnoreStart
//
// This method always throws an exception, so we ignore the closing
// brace
}
// @codeCoverageIgnoreEnd
/**
* Sets an item through ArrayAccess.
*
* This method just forwards the request to the inner iterator
*
* @param int $offset
* @return void
*/
public function offsetUnset($offset) {
$iterator = $this->getIterator();
$iterator->offsetUnset($offset);
// @codeCoverageIgnoreStart
//
// This method always throws an exception, so we ignore the closing
// brace
}
// @codeCoverageIgnoreEnd
/* }}} */
}

373
vendor/sabre/vobject/lib/Parameter.php vendored Normal file
View File

@@ -0,0 +1,373 @@
<?php
namespace Sabre\VObject;
use
ArrayObject;
/**
* VObject Parameter
*
* This class represents a parameter. A parameter is always tied to a property.
* In the case of:
* DTSTART;VALUE=DATE:20101108
* VALUE=DATE would be the parameter name and value.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Parameter extends Node {
/**
* Parameter name
*
* @var string
*/
public $name;
/**
* vCard 2.1 allows parameters to be encoded without a name.
*
* We can deduce the parameter name based on it's value.
*
* @var bool
*/
public $noName = false;
/**
* Parameter value
*
* @var string
*/
protected $value;
/**
* Sets up the object.
*
* It's recommended to use the create:: factory method instead.
*
* @param string $name
* @param string $value
*/
public function __construct(Document $root, $name, $value = null) {
$this->name = strtoupper($name);
$this->root = $root;
if (is_null($name)) {
$this->noName = true;
$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);
}
}
/**
* 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
* these are rather silly in 2014 and would probably rarely be
* used, but we like to be complete.
*
* @param string $value
* @return string
*/
public static function guessParameterNameByValue($value) {
switch(strtoupper($value)) {
// Encodings
case '7-BIT' :
case 'QUOTED-PRINTABLE' :
case 'BASE64' :
$name = 'ENCODING';
break;
// Common types
case 'WORK' :
case 'HOME' :
case 'PREF' :
// Delivery Label Type
case 'DOM' :
case 'INTL' :
case 'POSTAL' :
case 'PARCEL' :
// Telephone types
case 'VOICE' :
case 'FAX' :
case 'MSG' :
case 'CELL' :
case 'PAGER' :
case 'BBS' :
case 'MODEM' :
case 'CAR' :
case 'ISDN' :
case 'VIDEO' :
// EMAIL types (lol)
case 'AOL' :
case 'APPLELINK' :
case 'ATTMAIL' :
case 'CIS' :
case 'EWORLD' :
case 'INTERNET' :
case 'IBMMAIL' :
case 'MCIMAIL' :
case 'POWERSHARE' :
case 'PRODIGY' :
case 'TLX' :
case 'X400' :
// Photo / Logo format types
case 'GIF' :
case 'CGM' :
case 'WMF' :
case 'BMP' :
case 'DIB' :
case 'PICT' :
case 'TIFF' :
case 'PDF ':
case 'PS' :
case 'JPEG' :
case 'MPEG' :
case 'MPEG2' :
case 'AVI' :
case 'QTIME' :
// Sound Digital Audio Type
case 'WAVE' :
case 'PCM' :
case 'AIFF' :
// Key types
case 'X509' :
case 'PGP' :
$name = 'TYPE';
break;
// Value types
case 'INLINE' :
case 'URL' :
case 'CONTENT-ID' :
case 'CID' :
$name = 'VALUE';
break;
default:
$name = '';
}
return $name;
}
/**
* Updates the current value.
*
* This may be either a single, or multiple strings in an array.
*
* @param string|array $value
* @return void
*/
public function setValue($value) {
$this->value = $value;
}
/**
* Returns the current value
*
* This method will always return a string, or null. If there were multiple
* values, it will automatically concatinate them (separated by comma).
*
* @return string|null
*/
public function getValue() {
if (is_array($this->value)) {
return implode(',' , $this->value);
} else {
return $this->value;
}
}
/**
* Sets multiple values for this parameter.
*
* @param array $value
* @return void
*/
public function setParts(array $value) {
$this->value = $value;
}
/**
* Returns all values for this parameter.
*
* If there were no values, an empty array will be returned.
*
* @return array
*/
public function getParts() {
if (is_array($this->value)) {
return $this->value;
} elseif (is_null($this->value)) {
return array();
} else {
return array($this->value);
}
}
/**
* Adds a value to this parameter
*
* If the argument is specified as an array, all items will be added to the
* parameter value list.
*
* @param string|array $part
* @return void
*/
public function addValue($part) {
if (is_null($this->value)) {
$this->value = $part;
} else {
$this->value = array_merge((array)$this->value, (array)$part);
}
}
/**
* Checks if this parameter contains the specified value.
*
* This is a case-insensitive match. It makes sense to call this for for
* instance the TYPE parameter, to see if it contains a keyword such as
* 'WORK' or 'FAX'.
*
* @param string $value
* @return bool
*/
public function has($value) {
return in_array(
strtolower($value),
array_map('strtolower', (array)$this->value)
);
}
/**
* Turns the object back into a serialized blob.
*
* @return string
*/
public function serialize() {
$value = $this->getParts();
if (count($value)===0) {
return $this->name . '=';
}
if ($this->root->getDocumentType() === Document::VCARD21 && $this->noName) {
return implode(';', $value);
}
return $this->name . '=' . array_reduce(
$value,
function($out, $item) {
if (!is_null($out)) $out.=',';
// If there's no special characters in the string, we'll use the simple
// format.
//
// 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)
// severaly trips on + characters not being quoted, so we
// added + as well.
if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) {
return $out.$item;
} else {
// Enclosing in double-quotes, and using RFC6868 for encoding any
// special characters
$out.='"' . strtr(
$item,
array(
'^' => '^^',
"\n" => '^n',
'"' => '^\'',
)
) . '"';
return $out;
}
}
);
}
/**
* This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents.
*
* @return array
*/
public function jsonSerialize() {
return $this->value;
}
/**
* Called when this object is being cast to a string
*
* @return string
*/
public function __toString() {
return (string)$this->getValue();
}
/**
* Returns the iterator for this object
*
* @return ElementList
*/
public function getIterator() {
if (!is_null($this->iterator))
return $this->iterator;
return $this->iterator = new ArrayObject((array)$this->value);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Sabre\VObject;
/**
* Exception thrown by Reader if an invalid object was attempted to be parsed.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ParseException extends \Exception {
}

194
vendor/sabre/vobject/lib/Parser/Json.php vendored Normal file
View File

@@ -0,0 +1,194 @@
<?php
namespace Sabre\VObject\Parser;
use
Sabre\VObject\Component\VCalendar,
Sabre\VObject\Component\VCard,
Sabre\VObject\ParseException,
Sabre\VObject\EofException;
/**
* Json Parser.
*
* This parser parses both the jCal and jCard formats.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Json extends Parser {
/**
* The input data
*
* @var array
*/
protected $input;
/**
* Root component
*
* @var Document
*/
protected $root;
/**
* This method starts the parsing process.
*
* If the input was not supplied during construction, it's possible to pass
* it here instead.
*
* If either input or options are not supplied, the defaults will be used.
*
* @param resource|string|array|null $input
* @param int|null $options
* @return array
*/
public function parse($input = null, $options = null) {
if (!is_null($input)) {
$this->setInput($input);
}
if (is_null($this->input)) {
throw new EofException('End of input stream, or no input supplied');
}
if (!is_null($options)) {
$this->options = $options;
}
switch($this->input[0]) {
case 'vcalendar' :
$this->root = new VCalendar(array(), false);
break;
case 'vcard' :
$this->root = new VCard(array(), false);
break;
default :
throw new ParseException('The root component must either be a vcalendar, or a vcard');
}
foreach($this->input[1] as $prop) {
$this->root->add($this->parseProperty($prop));
}
if (isset($this->input[2])) foreach($this->input[2] as $comp) {
$this->root->add($this->parseComponent($comp));
}
// Resetting the input so we can throw an feof exception the next time.
$this->input = null;
return $this->root;
}
/**
* Parses a component
*
* @param array $jComp
* @return \Sabre\VObject\Component
*/
public function parseComponent(array $jComp) {
// We can remove $self from PHP 5.4 onward.
$self = $this;
$properties = array_map(
function($jProp) use ($self) {
return $self->parseProperty($jProp);
},
$jComp[1]
);
if (isset($jComp[2])) {
$components = array_map(
function($jComp) use ($self) {
return $self->parseComponent($jComp);
},
$jComp[2]
);
} else $components = array();
return $this->root->createComponent(
$jComp[0],
array_merge($properties, $components),
$defaults = false
);
}
/**
* Parses properties.
*
* @param array $jProp
* @return \Sabre\VObject\Property
*/
public function parseProperty(array $jProp) {
list(
$propertyName,
$parameters,
$valueType
) = $jProp;
$propertyName = strtoupper($propertyName);
// This is the default class we would be using if we didn't know the
// value type. We're using this value later in this function.
$defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName);
$parameters = (array)$parameters;
$value = array_slice($jProp, 3);
$valueType = strtoupper($valueType);
if (isset($parameters['group'])) {
$propertyName = $parameters['group'] . '.' . $propertyName;
unset($parameters['group']);
}
$prop = $this->root->createProperty($propertyName, null, $parameters, $valueType);
$prop->setJsonValue($value);
// We have to do something awkward here. FlatText as well as Text
// 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
// again.
if ($defaultPropertyClass === 'Sabre\VObject\Property\FlatText') {
$defaultPropertyClass = 'Sabre\VObject\Property\Text';
}
// 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=
// parameter.
if ($defaultPropertyClass !== get_class($prop)) {
$prop["VALUE"] = $valueType;
}
return $prop;
}
/**
* Sets the input data
*
* @param resource|string|array $input
* @return void
*/
public function setInput($input) {
if (is_resource($input)) {
$input = stream_get_contents($input);
}
if (is_string($input)) {
$input = json_decode($input);
}
$this->input = $input;
}
}

View File

@@ -0,0 +1,628 @@
<?php
namespace Sabre\VObject\Parser;
use
Sabre\VObject\ParseException,
Sabre\VObject\EofException,
Sabre\VObject\Component,
Sabre\VObject\Property,
Sabre\VObject\Component\VCalendar,
Sabre\VObject\Component\VCard;
/**
* MimeDir parser.
*
* 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:
*
* Sabre\VObject\Component\VCalendar
* Sabre\VObject\Component\VCard
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class MimeDir extends Parser {
/**
* The input stream.
*
* @var resource
*/
protected $input;
/**
* Root component
*
* @var Component
*/
protected $root;
/**
* Parses an iCalendar or vCard file
*
* Pass a stream or a string. If null is parsed, the existing buffer is
* used.
*
* @param string|resource|null $input
* @param int|null $options
* @return array
*/
public function parse($input = null, $options = null) {
$this->root = null;
if (!is_null($input)) {
$this->setInput($input);
}
if (!is_null($options)) $this->options = $options;
$this->parseDocument();
return $this->root;
}
/**
* Sets the input buffer. Must be a string or stream.
*
* @param resource|string $input
* @return void
*/
public function setInput($input) {
// Resetting the parser
$this->lineIndex = 0;
$this->startLine = 0;
if (is_string($input)) {
// Convering to a stream.
$stream = fopen('php://temp', 'r+');
fwrite($stream, $input);
rewind($stream);
$this->input = $stream;
} elseif (is_resource($input)) {
$this->input = $input;
} else {
throw new \InvalidArgumentException('This parser can only read from strings or streams.');
}
}
/**
* Parses an entire document.
*
* @return void
*/
protected function parseDocument() {
$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)
&& ord($line[0]) === 0xef
&& ord($line[1]) === 0xbb
&& ord($line[2]) === 0xbf) {
$line = substr($line, 3);
}
switch(strtoupper($line)) {
case 'BEGIN:VCALENDAR' :
$class = isset(VCalendar::$componentMap['VCALENDAR'])
? VCalendar::$componentMap[$name]
: 'Sabre\\VObject\\Component\\VCalendar';
break;
case 'BEGIN:VCARD' :
$class = isset(VCard::$componentMap['VCARD'])
? VCard::$componentMap['VCARD']
: 'Sabre\\VObject\\Component\\VCard';
break;
default :
throw new ParseException('This parser only supports VCARD and VCALENDAR files');
}
$this->root = new $class(array(), false);
while(true) {
// Reading until we hit END:
$line = $this->readLine();
if (strtoupper(substr($line,0,4)) === 'END:') {
break;
}
$result = $this->parseLine($line);
if ($result) {
$this->root->add($result);
}
}
$name = strtoupper(substr($line, 4));
if ($name!==$this->root->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
* the entire component
*
* @param string $line Unfolded line
* @return Node
*/
protected function parseLine($line) {
// Start of a new component
if (strtoupper(substr($line, 0, 6)) === 'BEGIN:') {
$component = $this->root->createComponent(substr($line,6), array(), false);
while(true) {
// Reading until we hit END:
$line = $this->readLine();
if (strtoupper(substr($line,0,4)) === 'END:') {
break;
}
$result = $this->parseLine($line);
if ($result) {
$component->add($result);
}
}
$name = strtoupper(substr($line, 4));
if ($name!==$component->name) {
throw new ParseException('Invalid MimeDir file. expected: "END:' . $component->name . '" got: "END:' . $name . '"');
}
return $component;
} else {
// Property reader
$property = $this->readProperty($line);
if (!$property) {
// Ignored line
return false;
}
return $property;
}
}
/**
* We need to look ahead 1 line every time to see if we need to 'unfold'
* the next line.
*
* If that was not the case, we store it here.
*
* @var null|string
*/
protected $lineBuffer;
/**
* The real current line number.
*/
protected $lineIndex = 0;
/**
* In the case of unfolded lines, this property holds the line number for
* the start of the line.
*
* @var int
*/
protected $startLine = 0;
/**
* Contains a 'raw' representation of the current line.
*
* @var string
*/
protected $rawLine;
/**
* Reads a single line from the buffer.
*
* This method strips any newlines and also takes care of unfolding.
*
* @throws \Sabre\VObject\EofException
* @return string
*/
protected function readLine() {
if (!is_null($this->lineBuffer)) {
$rawLine = $this->lineBuffer;
$this->lineBuffer = null;
} else {
do {
$eof = feof($this->input);
$rawLine = fgets($this->input);
if ($eof || (feof($this->input) && $rawLine===false)) {
throw new EofException('End of document reached prematurely');
}
if ($rawLine === false) {
throw new ParseException('Error reading from input stream');
}
$rawLine = rtrim($rawLine, "\r\n");
} while ($rawLine === ''); // Skipping empty lines
$this->lineIndex++;
}
$line = $rawLine;
$this->startLine = $this->lineIndex;
// Looking ahead for folded lines.
while (true) {
$nextLine = rtrim(fgets($this->input), "\r\n");
$this->lineIndex++;
if (!$nextLine) {
break;
}
if ($nextLine[0] === "\t" || $nextLine[0] === " ") {
$line .= substr($nextLine, 1);
$rawLine .= "\n " . substr($nextLine, 1);
} else {
$this->lineBuffer = $nextLine;
break;
}
}
$this->rawLine = $rawLine;
return $line;
}
/**
* Reads a property or component from a line.
*
* @return void
*/
protected function readProperty($line) {
if ($this->options & self::OPTION_FORGIVING) {
$propNameToken = 'A-Z0-9\-\._\\/';
} else {
$propNameToken = 'A-Z0-9\-\.';
}
$paramNameToken = 'A-Z0-9\-';
$safeChar = '^";:,';
$qSafeChar = '^"';
$regex = "/
^(?P<name> [$propNameToken]+ ) (?=[;:]) # property name
|
(?<=:)(?P<propValue> .+)$ # property value
|
;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name
|
(=|,)(?P<paramValue> # parameter value
(?: [$safeChar]*) |
\"(?: [$qSafeChar]+)\"
) (?=[;:,])
/xi";
//echo $regex, "\n"; die();
preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
$property = array(
'name' => null,
'parameters' => array(),
'value' => null
);
$lastParam = null;
/**
* Looping through all the tokens.
*
* Note that we are looping through them in reverse order, because if a
* sub-pattern matched, the subsequent named patterns will not show up
* in the result.
*/
foreach($matches as $match) {
if (isset($match['paramValue'])) {
if ($match['paramValue'] && $match['paramValue'][0] === '"') {
$value = substr($match['paramValue'], 1, -1);
} else {
$value = $match['paramValue'];
}
$value = $this->unescapeParam($value);
if (is_null($property['parameters'][$lastParam])) {
$property['parameters'][$lastParam] = $value;
} elseif (is_array($property['parameters'][$lastParam])) {
$property['parameters'][$lastParam][] = $value;
} else {
$property['parameters'][$lastParam] = array(
$property['parameters'][$lastParam],
$value
);
}
continue;
}
if (isset($match['paramName'])) {
$lastParam = strtoupper($match['paramName']);
if (!isset($property['parameters'][$lastParam])) {
$property['parameters'][$lastParam] = null;
}
continue;
}
if (isset($match['propValue'])) {
$property['value'] = $match['propValue'];
continue;
}
if (isset($match['name']) && $match['name']) {
$property['name'] = strtoupper($match['name']);
continue;
}
// @codeCoverageIgnoreStart
throw new \LogicException('This code should not be reachable');
// @codeCoverageIgnoreEnd
}
if (is_null($property['value'])) {
$property['value'] = '';
}
if (!$property['name']) {
if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
return false;
}
throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions');
}
// 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.
//
// Our parser will get those as parameters without a value instead, so
// we're filtering these parameters out first.
$namedParameters = array();
$namelessParameters = array();
foreach($property['parameters'] as $name=>$value) {
if (!is_null($value)) {
$namedParameters[$name] = $value;
} else {
$namelessParameters[] = $name;
}
}
$propObj = $this->root->createProperty($property['name'], null, $namedParameters);
foreach($namelessParameters as $namelessParameter) {
$propObj->add(null, $namelessParameter);
}
if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') {
$propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
} else {
$propObj->setRawMimeDirValue($property['value']);
}
return $propObj;
}
/**
* Unescapes a property value.
*
* vCard 2.1 says:
* * Semi-colons must be escaped in some property values, specifically
* ADR, ORG and N.
* * Semi-colons must be escaped in parameter values, because semi-colons
* are also use to separate values.
* * No mention of escaping backslashes with another backslash.
* * newlines are not escaped either, instead QUOTED-PRINTABLE is used to
* span values over more than 1 line.
*
* vCard 3.0 says:
* * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
* escaped, all time time.
* * Comma's are used for delimeters in multiple values
* * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
* as in some properties semi-colon is used for separators.
* * Properties using semi-colons: N, ADR, GEO, ORG
* * Both ADR and N's individual parts may be broken up further with a
* comma.
* * Properties using commas: NICKNAME, CATEGORIES
*
* vCard 4.0 (rfc6350) says:
* * Commas must be escaped.
* * Semi-colons may be escaped, an unescaped semi-colon _may_ be a
* delimiter, depending on the property.
* * Backslashes must be escaped
* * Newlines must be escaped as either \N or \n.
* * Some compound properties may contain multiple parts themselves, so a
* comma within a semi-colon delimited property may also be unescaped
* to denote multiple parts _within_ the compound property.
* * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP.
* * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID.
*
* Even though the spec says that commas must always be escaped, the
* example for GEO in Section 6.5.2 seems to violate this.
*
* iCalendar 2.0 (rfc5545) says:
* * Commas or semi-colons may be used as delimiters, depending on the
* property.
* * Commas, semi-colons, backslashes, newline (\N or \n) are always
* escaped, unless they are delimiters.
* * Colons shall not be escaped.
* * Commas can be considered the 'default delimiter' and is described as
* the delimiter in cases where the order of the multiple values is
* insignificant.
* * Semi-colons are described as the delimiter for 'structured values'.
* They are specifically used in Semi-colons are used as a delimiter in
* REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however.
*
* Now for the parameters
*
* If delimiter is not set (null) this method will just return a string.
* If it's a comma or a semi-colon the string will be split on those
* characters, and always return an array.
*
* @param string $input
* @param string $delimiter
* @return string|string[]
*/
static public function unescapeValue($input, $delimiter = ';') {
$regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
if ($delimiter) {
$regex .= ' | (' . $delimiter . ')';
}
$regex .= ') #x';
$matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$resultArray = array();
$result = '';
foreach($matches as $match) {
switch ($match) {
case '\\\\' :
$result .='\\';
break;
case '\N' :
case '\n' :
$result .="\n";
break;
case '\;' :
$result .=';';
break;
case '\,' :
$result .=',';
break;
case $delimiter :
$resultArray[] = $result;
$result = '';
break;
default :
$result .= $match;
break;
}
}
$resultArray[] = $result;
return $delimiter ? $resultArray : $result;
}
/**
* Unescapes a parameter value.
*
* vCard 2.1:
* * Does not mention a mechanism for this. In addition, double quotes
* are never used to wrap values.
* * This means that parameters can simply not contain colons or
* semi-colons.
*
* vCard 3.0 (rfc2425, rfc2426):
* * Parameters _may_ be surrounded by double quotes.
* * If this is not the case, semi-colon, colon and comma may simply not
* occur (the comma used for multiple parameter values though).
* * If it is surrounded by double-quotes, it may simply not contain
* double-quotes.
* * This means that a parameter can in no case encode double-quotes, or
* newlines.
*
* vCard 4.0 (rfc6350)
* * Behavior seems to be identical to vCard 3.0
*
* iCalendar 2.0 (rfc5545)
* * Behavior seems to be identical to vCard 3.0
*
* Parameter escaping mechanism (rfc6868) :
* * This rfc describes a new way to escape parameter values.
* * New-line is encoded as ^n
* * ^ is encoded as ^^.
* * " is encoded as ^'
*
* @param string $input
* @return void
*/
private function unescapeParam($input) {
return
preg_replace_callback(
'#(\^(\^|n|\'))#',
function($matches) {
switch($matches[2]) {
case 'n' :
return "\n";
case '^' :
return '^';
case '\'' :
return '"';
// @codeCoverageIgnoreStart
}
// @codeCoverageIgnoreEnd
},
$input
);
}
/**
* Gets the full quoted printable value.
*
* We need a special method for this, because newlines have both a meaning
* in vCards, and in QuotedPrintable.
*
* This method does not do any decoding.
*
* @return string
*/
private function extractQuotedPrintableValue() {
// 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
// skip over the parameters first, as they may contain one.
$regex = '/^
(?: [^:])+ # Anything but a colon
(?: "[^"]")* # A parameter in double quotes
: # start of the value we really care about
(.*)$
/xs';
preg_match($regex, $this->rawLine, $matches);
$value = $matches[1];
// Removing the first whitespace character from every line. Kind of
// like unfolding, but we keep the newline.
$value = str_replace("\n ", "\n", $value);
// Microsoft products don't always correctly fold lines, they may be
// missing a whitespace. So if 'forgiving' is turned on, we will take
// those as well.
if ($this->options & self::OPTION_FORGIVING) {
while(substr($value,-1) === '=') {
// Reading the line
$this->readLine();
// Grabbing the raw form
$value.="\n" . $this->rawLine;
}
}
return $value;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Sabre\VObject\Parser;
/**
* Abstract parser.
*
* This class serves as a base-class for the different parsers.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class Parser {
/**
* Turning on this option makes the parser more forgiving.
*
* In the case of the MimeDir parser, this means that the parser will
* accept slashes and underscores in property names, and it will also
* attempt to fix Microsoft vCard 2.1's broken line folding.
*/
const OPTION_FORGIVING = 1;
/**
* If this option is turned on, any lines we cannot parse will be ignored
* by the reader.
*/
const OPTION_IGNORE_INVALID_LINES = 2;
/**
* Bitmask of parser options
*
* @var int
*/
protected $options;
/**
* Creates the parser.
*
* Optionally, it's possible to parse the input stream here.
*
* @param mixed $input
* @param int $options Any parser options (OPTION constants).
* @return void
*/
public function __construct($input = null, $options = 0) {
if (!is_null($input)) {
$this->setInput($input);
}
$this->options = $options;
}
/**
* This method starts the parsing process.
*
* If the input was not supplied during construction, it's possible to pass
* it here instead.
*
* If either input or options are not supplied, the defaults will be used.
*
* @param mixed $input
* @param int|null $options
* @return array
*/
abstract public function parse($input = null, $options = null);
/**
* Sets the input data
*
* @param mixed $input
* @return void
*/
abstract public function setInput($input);
}

555
vendor/sabre/vobject/lib/Property.php vendored Normal file
View File

@@ -0,0 +1,555 @@
<?php
namespace Sabre\VObject;
/**
* Property
*
* A property is always in a KEY:VALUE structure, and may optionally contain
* parameters.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class Property extends Node {
/**
* Property name.
*
* This will contain a string such as DTSTART, SUMMARY, FN.
*
* @var string
*/
public $name;
/**
* Property group.
*
* This is only used in vcards
*
* @var string
*/
public $group;
/**
* List of parameters
*
* @var array
*/
public $parameters = array();
/**
* Current value
*
* @var mixed
*/
protected $value;
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = ';';
/**
* Creates the generic property.
*
* Parameters must be specified in key=>value syntax.
*
* @param Component $root The root document
* @param string $name
* @param string|array|null $value
* @param array $parameters List of parameters
* @param string $group The vcard property group
* @return void
*/
function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) {
$this->name = $name;
$this->group = $group;
$this->root = $root;
foreach($parameters as $k=>$v) {
$this->add($k, $v);
}
if (!is_null($value)) {
$this->setValue($value);
}
}
/**
* Updates the current value.
*
* This may be either a single, or multiple strings in an array.
*
* @param string|array $value
* @return void
*/
function setValue($value) {
$this->value = $value;
}
/**
* Returns the current value.
*
* This method will always return a singular value. If this was a
* multi-value object, some decision will be made first on how to represent
* it as a string.
*
* To get the correct multi-value version, use getParts.
*
* @return string
*/
function getValue() {
if (is_array($this->value)) {
if (count($this->value)==0) {
return null;
} elseif (count($this->value)===1) {
return $this->value[0];
} else {
return $this->getRawMimeDirValue($this->value);
}
} else {
return $this->value;
}
}
/**
* Sets a multi-valued property.
*
* @param array $parts
* @return void
*/
function setParts(array $parts) {
$this->value = $parts;
}
/**
* Returns a multi-valued property.
*
* This method always returns an array, if there was only a single value,
* it will still be wrapped in an array.
*
* @return array
*/
function getParts() {
if (is_null($this->value)) {
return array();
} elseif (is_array($this->value)) {
return $this->value;
} else {
return array($this->value);
}
}
/**
* Adds a new parameter, and returns the new item.
*
* If a parameter with same name already existed, the values will be
* combined.
* If nameless parameter is added, we try to guess it's name.
*
* @param string $name
* @param string|null|array $value
* @return Node
*/
function add($name, $value = null) {
$noName = false;
if ($name === null) {
$name = Parameter::guessParameterNameByValue($value);
$noName = true;
}
if (isset($this->parameters[strtoupper($name)])) {
$this->parameters[strtoupper($name)]->addValue($value);
}
else {
$param = new Parameter($this->root, $name, $value);
$param->noName = $noName;
$this->parameters[$param->name] = $param;
}
}
/**
* Returns an iterable list of children
*
* @return array
*/
function parameters() {
return $this->parameters;
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
abstract function getValueType();
/**
* 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
* @return void
*/
abstract function setRawMimeDirValue($val);
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
abstract function getRawMimeDirValue();
/**
* Turns the object back into a serialized blob.
*
* @return string
*/
function serialize() {
$str = $this->name;
if ($this->group) $str = $this->group . '.' . $this->name;
foreach($this->parameters as $param) {
$str.=';' . $param->serialize();
}
$str.=':' . $this->getRawMimeDirValue();
$out = '';
while(strlen($str)>0) {
if (strlen($str)>75) {
$out.= mb_strcut($str,0,75,'utf-8') . "\r\n";
$str = ' ' . mb_strcut($str,75,strlen($str),'utf-8');
} else {
$out.=$str . "\r\n";
$str='';
break;
}
}
return $out;
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
function getJsonValue() {
return $this->getParts();
}
/**
* Sets the json value, as it would appear in a jCard or jCal object.
*
* The value must always be an array.
*
* @param array $value
* @return void
*/
function setJsonValue(array $value) {
if (count($value)===1) {
$this->setValue(reset($value));
} else {
$this->setValue($value);
}
}
/**
* This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents.
*
* @return array
*/
function jsonSerialize() {
$parameters = array();
foreach($this->parameters as $parameter) {
if ($parameter->name === 'VALUE') {
continue;
}
$parameters[strtolower($parameter->name)] = $parameter->jsonSerialize();
}
// In jCard, we need to encode the property-group as a separate 'group'
// parameter.
if ($this->group) {
$parameters['group'] = $this->group;
}
return array_merge(
array(
strtolower($this->name),
(object)$parameters,
strtolower($this->getValueType()),
),
$this->getJsonValue()
);
}
/**
* Called when this object is being cast to a string.
*
* If the property only had a single value, you will get just that. In the
* case the property had multiple values, the contents will be escaped and
* combined with ,.
*
* @return string
*/
function __toString() {
return (string)$this->getValue();
}
/* ArrayAccess interface {{{ */
/**
* Checks if an array element exists
*
* @param mixed $name
* @return bool
*/
function offsetExists($name) {
if (is_int($name)) return parent::offsetExists($name);
$name = strtoupper($name);
foreach($this->parameters as $parameter) {
if ($parameter->name == $name) return true;
}
return false;
}
/**
* Returns a parameter.
*
* If the parameter does not exist, null is returned.
*
* @param string $name
* @return Node
*/
function offsetGet($name) {
if (is_int($name)) return parent::offsetGet($name);
$name = strtoupper($name);
if (!isset($this->parameters[$name])) {
return null;
}
return $this->parameters[$name];
}
/**
* Creates a new parameter
*
* @param string $name
* @param mixed $value
* @return void
*/
function offsetSet($name, $value) {
if (is_int($name)) {
parent::offsetSet($name, $value);
// @codeCoverageIgnoreStart
// This will never be reached, because an exception is always
// thrown.
return;
// @codeCoverageIgnoreEnd
}
$param = new Parameter($this->root, $name, $value);
$this->parameters[$param->name] = $param;
}
/**
* Removes one or more parameters with the specified name
*
* @param string $name
* @return void
*/
function offsetUnset($name) {
if (is_int($name)) {
parent::offsetUnset($name);
// @codeCoverageIgnoreStart
// This will never be reached, because an exception is always
// thrown.
return;
// @codeCoverageIgnoreEnd
}
unset($this->parameters[strtoupper($name)]);
}
/* }}} */
/**
* This method is automatically called when the object is cloned.
* Specifically, this will ensure all child elements are also cloned.
*
* @return void
*/
function __clone() {
foreach($this->parameters as $key=>$child) {
$this->parameters[$key] = clone $child;
$this->parameters[$key]->parent = $this;
}
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* - Node::REPAIR - If something is broken, and automatic repair may
* be attempted.
*
* An array is returned with warnings.
*
* Every item in the array has the following properties:
* * level - (number between 1 and 3 with severity information)
* * message - (human readable message)
* * node - (reference to the offending node)
*
* @param int $options
* @return array
*/
function validate($options = 0) {
$warnings = array();
// Checking if our value is UTF-8
if (!StringUtil::isUTF8($this->getRawMimeDirValue())) {
$oldValue = $this->getRawMimeDirValue();
$level = 3;
if ($options & self::REPAIR) {
$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[] = array(
'level' => $level,
'message' => $message,
'node' => $this,
);
}
// Checking if the propertyname does not contain any invalid bytes.
if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
$warnings[] = array(
'level' => 1,
'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed',
'node' => $this,
);
if ($options & self::REPAIR) {
// Uppercasing and converting underscores to dashes.
$this->name = strtoupper(
str_replace('_', '-', $this->name)
);
// Removing every other invalid character
$this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
}
}
if ($encoding = $this->offsetGet('ENCODING')) {
if ($this->root->getDocumentType()===Document::VCARD40) {
$warnings[] = array(
'level' => 1,
'message' => 'ENCODING parameter is not valid in vCard 4.',
'node' => $this
);
} else {
$encoding = (string)$encoding;
$allowedEncoding = array();
switch($this->root->getDocumentType()) {
case Document::ICALENDAR20 :
$allowedEncoding = array('8BIT', 'BASE64');
break;
case Document::VCARD21 :
$allowedEncoding = array('QUOTED-PRINTABLE', 'BASE64', '8BIT');
break;
case Document::VCARD30 :
$allowedEncoding = array('B');
break;
}
if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) {
$warnings[] = array(
'level' => 1,
'message' => 'ENCODING=' . strtoupper($encoding) . ' is not valid for this document type.',
'node' => $this
);
}
}
}
// Validating inner parameters
foreach($this->parameters as $param) {
$warnings = array_merge($warnings, $param->validate($options));
}
return $warnings;
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Sabre\VObject\Property;
use
LogicException,
Sabre\VObject\Property;
/**
* BINARY property
*
* This object represents BINARY values.
*
* Binary values are most commonly used by the iCalendar ATTACH property, and
* the vCard PHOTO property.
*
* This property will transparently encode and decode to base64.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Binary extends Property {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = null;
/**
* Updates the current value.
*
* This may be either a single, or multiple strings in an array.
*
* @param string|array $value
* @return void
*/
public function setValue($value) {
if(is_array($value)) {
if(count($value) === 1) {
$this->value = $value[0];
} else {
throw new \InvalidArgumentException('The argument must either be a string or an array with only one child');
}
} else {
$this->value = $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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->value = base64_decode($val);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return base64_encode($this->value);
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return 'BINARY';
}
/**
* 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(base64_encode($this->getValue()));
}
/**
* Sets the json value, as it would appear in a jCard or jCal object.
*
* The value must always be an array.
*
* @param array $value
* @return void
*/
public function setJsonValue(array $value) {
$value = array_map('base64_decode', $value);
parent::setJsonValue($value);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Sabre\VObject\Property;
use
Sabre\VObject\Property;
/**
* Boolean property
*
* This object represents BOOLEAN values. These are always the case-insenstive
* string TRUE or FALSE.
*
* Automatic conversion to PHP's true and false are done.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Boolean extends Property {
/**
* 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
* @return void
*/
public function setRawMimeDirValue($val) {
$val = strtoupper($val)==='TRUE'?true:false;
$this->setValue($val);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return $this->value?'TRUE':'FALSE';
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return 'BOOLEAN';
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Sabre\VObject\Property;
/**
* FlatText property
*
* This object represents certain TEXT values.
*
* Specifically, this property is used for text values where there is only 1
* part. Semi-colons and colons will be de-escaped when deserializing, but if
* any semi-colons or commas appear without a backslash, we will not assume
* that they are delimiters.
*
* vCard 2.1 specifically has a whole bunch of properties where this may
* happen, as it only defines a delimiter for a few properties.
*
* vCard 4.0 states something similar. An unescaped semi-colon _may_ be a
* delimiter, depending on the property.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class FlatText extends Text {
/**
* Field separator
*
* @var string
*/
public $delimiter = ',';
/**
* Sets the value as a quoted-printable encoded string.
*
* Overriding this so we're not splitting on a ; delimiter.
*
* @param string $val
* @return void
*/
public function setQuotedPrintableValue($val) {
$val = quoted_printable_decode($val);
$this->setValue($val);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Sabre\VObject\Property;
use
Sabre\VObject\Property;
/**
* Float property
*
* This object represents FLOAT values. These can be 1 or more floating-point
* numbers.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class FloatValue extends Property {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = ';';
/**
* 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
* @return void
*/
public function setRawMimeDirValue($val) {
$val = explode($this->delimiter, $val);
foreach($val as &$item) {
$item = (float)$item;
}
$this->setParts($val);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return implode(
$this->delimiter,
$this->getParts()
);
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "FLOAT";
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue() {
$val = array_map(
function($item) {
return (float)$item;
},
$this->getParts()
);
// Special-casing the GEO property.
//
// See:
// http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2
if ($this->name==='GEO') {
return array($val);
} else {
return $val;
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Sabre\VObject\Property\ICalendar;
use
Sabre\VObject\Property\Text;
/**
* CalAddress property
*
* This object encodes CAL-ADDRESS values, as defined in rfc5545
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalAddress extends Text {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = null;
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
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

@@ -0,0 +1,18 @@
<?php
namespace Sabre\VObject\Property\ICalendar;
/**
* DateTime property
*
* This object represents DATE values, as defined here:
*
* http://tools.ietf.org/html/rfc5545#section-3.3.5
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Date extends DateTime {
}

View File

@@ -0,0 +1,390 @@
<?php
namespace Sabre\VObject\Property\ICalendar;
use DateTimeZone;
use Sabre\VObject\Property;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\TimeZoneUtil;
/**
* DateTime property
*
* This object represents DATE-TIME values, as defined here:
*
* http://tools.ietf.org/html/rfc5545#section-3.3.4
*
* This particular object has a bit of hackish magic that it may also in some
* cases represent a DATE value. This is because it's a common usecase to be
* able to change a DATE-TIME into a DATE.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class DateTime extends Property {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = ',';
/**
* Sets a multi-valued property.
*
* You may also specify DateTime objects here.
*
* @param array $parts
* @return void
*/
public function setParts(array $parts) {
if (isset($parts[0]) && $parts[0] instanceof \DateTime) {
$this->setDateTimes($parts);
} 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 DateTime here.
*
* @param string|array|\DateTime $value
* @return void
*/
public function setValue($value) {
if (is_array($value) && isset($value[0]) && $value[0] instanceof \DateTime) {
$this->setDateTimes($value);
} elseif ($value instanceof \DateTime) {
$this->setDateTimes(array($value));
} else {
parent::setValue($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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->setValue(explode($this->delimiter, $val));
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return implode($this->delimiter, $this->getParts());
}
/**
* Returns true if this is a DATE-TIME value, false if it's a DATE.
*
* @return bool
*/
public function hasTime() {
return strtoupper((string)$this['VALUE']) !== 'DATE';
}
/**
* 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']) &&
strpos($this->getValue(),'Z')===false
);
}
/**
* 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 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 \DateTime
*/
public function getDateTime(DateTimeZone $timeZone = null) {
$dt = $this->getDateTimes($timeZone);
if (!$dt) return null;
return $dt[0];
}
/**
* 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 \DateTime[]
*/
public function getDateTimes(DateTimeZone $timeZone = null) {
// Does the property have a TZID?
$tzid = $this['TZID'];
if ($tzid) {
$timeZone = TimeZoneUtil::getTimeZone((string)$tzid, $this->root);
}
$dts = array();
foreach($this->getParts() as $part) {
$dts[] = DateTimeParser::parse($part, $timeZone);
}
return $dts;
}
/**
* Sets the property as a DateTime object.
*
* @param \DateTime $dt
* @param bool isFloating If set to true, timezones will be ignored.
* @return void
*/
public function setDateTime(\DateTime $dt, $isFloating = false) {
$this->setDateTimes(array($dt), $isFloating);
}
/**
* Sets the property as multiple date-time objects.
*
* The first value will be used as a reference for the timezones, and all
* the otehr values will be adjusted for that timezone
*
* @param \DateTime[] $dt
* @param bool isFloating If set to true, timezones will be ignored.
* @return void
*/
public function setDateTimes(array $dt, $isFloating = false) {
$values = array();
if($this->hasTime()) {
$tz = null;
$isUtc = false;
foreach($dt as $d) {
if ($isFloating) {
$values[] = $d->format('Ymd\\THis');
continue;
}
if (is_null($tz)) {
$tz = $d->getTimeZone();
$isUtc = in_array($tz->getName() , array('UTC', 'GMT', 'Z'));
if (!$isUtc) {
$this->offsetSet('TZID', $tz->getName());
}
} else {
$d->setTimeZone($tz);
}
if ($isUtc) {
$values[] = $d->format('Ymd\\THis\\Z');
} else {
$values[] = $d->format('Ymd\\THis');
}
}
if ($isUtc || $isFloating) {
$this->offsetUnset('TZID');
}
} else {
foreach($dt as $d) {
$values[] = $d->format('Ymd');
}
$this->offsetUnset('TZID');
}
$this->value = $values;
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return $this->hasTime()?'DATE-TIME':'DATE';
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue() {
$dts = $this->getDateTimes();
$hasTime = $this->hasTime();
$isFloating = $this->isFloating();
$tz = $dts[0]->getTimeZone();
$isUtc = $isFloating ? false : in_array($tz->getName() , array('UTC', 'GMT', 'Z'));
return array_map(
function($dt) use ($hasTime, $isUtc) {
if ($hasTime) {
return $dt->format('Y-m-d\\TH:i:s') . ($isUtc?'Z':'');
} else {
return $dt->format('Y-m-d');
}
},
$dts
);
}
/**
* Sets the json value, as it would appear in a jCard or jCal object.
*
* The value must always be an array.
*
* @param array $value
* @return void
*/
public function setJsonValue(array $value) {
// dates and times in jCal have one difference to dates and times in
// iCalendar. In jCal date-parts are separated by dashes, and
// time-parts are separated by colons. It makes sense to just remove
// those.
$this->setValue(
array_map(
function($item) {
return strtr($item, array(':'=>'', '-'=>''));
},
$value
)
);
}
/**
* We need to intercept offsetSet, because it may be used to alter the
* VALUE from DATE-TIME to DATE or vice-versa.
*
* @param string $name
* @param mixed $value
* @return void
*/
public function offsetSet($name, $value) {
parent::offsetSet($name, $value);
if (strtoupper($name)!=='VALUE') {
return;
}
// This will ensure that dates are correctly encoded.
$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();
try {
foreach($values as $value) {
switch($valueType) {
case 'DATE' :
$foo = DateTimeParser::parseDate($value);
break;
case 'DATE-TIME' :
$foo = DateTimeParser::parseDateTime($value);
break;
}
}
} catch (\LogicException $e) {
$messages[] = array(
'level' => 3,
'message' => 'The supplied value (' . $value . ') is not a correct ' . $valueType,
'node' => $this,
);
}
return $messages;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Sabre\VObject\Property\ICalendar;
use
Sabre\VObject\Property,
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\DateTimeParser;
/**
* Duration property
*
* This object represents DURATION values, as defined here:
*
* http://tools.ietf.org/html/rfc5545#section-3.3.6
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Duration extends Property {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = ',';
/**
* 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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->setValue(explode($this->delimiter, $val));
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return implode($this->delimiter, $this->getParts());
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return 'DURATION';
}
/**
* Returns a DateInterval representation of the Duration property.
*
* If the property has more than one value, only the first is returned.
*
* @return \DateInterval
*/
public function getDateInterval() {
$parts = $this->getParts();
$value = $parts[0];
return DateTimeParser::parseDuration($value);
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Sabre\VObject\Property\ICalendar;
use
Sabre\VObject\Property,
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\DateTimeParser;
/**
* Period property
*
* This object represents PERIOD values, as defined here:
*
* http://tools.ietf.org/html/rfc5545#section-3.8.2.6
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Period extends Property {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = ',';
/**
* 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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->setValue(explode($this->delimiter, $val));
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return implode($this->delimiter, $this->getParts());
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "PERIOD";
}
/**
* Sets the json value, as it would appear in a jCard or jCal object.
*
* The value must always be an array.
*
* @param array $value
* @return void
*/
public function setJsonValue(array $value) {
$value = array_map(
function($item) {
return strtr(implode('/', $item), array(':' => '', '-' => ''));
},
$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();
foreach($this->getParts() as $item) {
list($start, $end) = explode('/', $item, 2);
$start = DateTimeParser::parseDateTime($start);
// This is a duration value.
if ($end[0]==='P') {
$return[] = array(
$start->format('Y-m-d\\TH:i:s'),
$end
);
} else {
$end = DateTimeParser::parseDateTime($end);
$return[] = array(
$start->format('Y-m-d\\TH:i:s'),
$end->format('Y-m-d\\TH:i:s'),
);
}
}
return $return;
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Sabre\VObject\Property\ICalendar;
use
Sabre\VObject\Property,
Sabre\VObject\Parser\MimeDir;
/**
* Recur property
*
* This object represents RECUR properties.
* These values are just used for RRULE and the now deprecated EXRULE.
*
* The RRULE property may look something like this:
*
* RRULE:FREQ=MONTHLY;BYDAY=1,2,3;BYHOUR=5.
*
* This property exposes this as a key=>value array that is accessible using
* getParts, and may be set using setParts.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Recur extends Property {
/**
* Updates the current value.
*
* This may be either a single, or multiple strings in an array.
*
* @param string|array $value
* @return void
*/
public function setValue($value) {
// If we're getting the data from json, we'll be receiving an object
if ($value instanceof \StdClass) {
$value = (array)$value;
}
if (is_array($value)) {
$newVal = array();
foreach($value as $k=>$v) {
if (is_string($v)) {
$v = strtoupper($v);
// The value had multiple sub-values
if (strpos($v,',')!==false) {
$v = explode(',', $v);
}
} else {
$v = array_map('strtoupper', $v);
}
$newVal[strtoupper($k)] = $v;
}
$this->value = $newVal;
} elseif (is_string($value)) {
$this->value = self::stringToArray($value);
} else {
throw new \InvalidArgumentException('You must either pass a string, or a key=>value array');
}
}
/**
* Returns the current value.
*
* This method will always return a singular value. If this was a
* multi-value object, some decision will be made first on how to represent
* it as a string.
*
* To get the correct multi-value version, use getParts.
*
* @return string
*/
public function getValue() {
$out = array();
foreach($this->value as $key=>$value) {
$out[] = $key . '=' . (is_array($value)?implode(',', $value):$value);
}
return strtoupper(implode(';',$out));
}
/**
* Sets a multi-valued property.
*
* @param array $parts
* @return void
*/
public function setParts(array $parts) {
$this->setValue($parts);
}
/**
* Returns a multi-valued property.
*
* This method always returns an array, if there was only a single value,
* it will still be wrapped in an array.
*
* @return array
*/
public function getParts() {
return $this->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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->setValue($val);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return $this->getValue();
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "RECUR";
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue() {
$values = array();
foreach($this->getParts() as $k=>$v) {
$values[strtolower($k)] = $v;
}
return array($values);
}
/**
* Parses an RRULE value string, and turns it into a struct-ish array.
*
* @param string $value
* @return array
*/
static function stringToArray($value) {
$value = strtoupper($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;
}
return $newValue;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Sabre\VObject\Property;
use
Sabre\VObject\Property;
/**
* Integer property
*
* This object represents INTEGER values. These are always a single integer.
* They may be preceeded by either + or -.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class IntegerValue extends Property {
/**
* 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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->setValue((int)$val);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return $this->value;
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "INTEGER";
}
/**
* 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((int)$this->getValue());
}
}

View File

@@ -0,0 +1,333 @@
<?php
namespace Sabre\VObject\Property;
use
Sabre\VObject\Property,
Sabre\VObject\Component,
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\Document;
/**
* Text property
*
* This object represents TEXT values.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Text extends Property {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string
*/
public $delimiter = ',';
/**
* List of properties that are considered 'structured'.
*
* @var array
*/
protected $structuredValues = array(
// vCard
'N',
'ADR',
'ORG',
'GENDER',
// iCalendar
'REQUEST-STATUS',
);
/**
* Some text components have a minimum number of components.
*
* N must for instance be represented as 5 components, separated by ;, even
* if the last few components are unused.
*
* @var array
*/
protected $minimumPropertyValues = array(
'N' => 5,
'ADR' => 7,
);
/**
* Creates the property.
*
* You can specify the parameters either in key=>value syntax, in which case
* parameters will automatically be created, or you can just pass a list of
* Parameter objects.
*
* @param Component $root The root document
* @param string $name
* @param string|array|null $value
* @param array $parameters List of parameters
* @param string $group The vcard property group
* @return void
*/
public function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) {
// There's two types of multi-valued text properties:
// 1. multivalue properties.
// 2. structured value properties
//
// The former is always separated by a comma, the latter by semi-colon.
if (in_array($name, $this->structuredValues)) {
$this->delimiter = ';';
}
parent::__construct($root, $name, $value, $parameters, $group);
}
/**
* 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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->setValue(MimeDir::unescapeValue($val, $this->delimiter));
}
/**
* Sets the value as a quoted-printable encoded string.
*
* @param string $val
* @return void
*/
public function setQuotedPrintableValue($val) {
$val = quoted_printable_decode($val);
// Quoted printable only appears in vCard 2.1, and the only character
// that may be escaped there is ;. So we are simply splitting on just
// that.
//
// We also don't have to unescape \\, so all we need to look for is a ;
// that's not preceeded with a \.
$regex = '# (?<!\\\\) ; #x';
$matches = preg_split($regex, $val);
$this->setValue($matches);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
$val = $this->getParts();
if (isset($this->minimumPropertyValues[$this->name])) {
$val = array_pad($val, $this->minimumPropertyValues[$this->name], '');
}
foreach($val as &$item) {
if (!is_array($item)) {
$item = array($item);
}
foreach($item as &$subItem) {
$subItem = strtr(
$subItem,
array(
'\\' => '\\\\',
';' => '\;',
',' => '\,',
"\n" => '\n',
"\r" => "",
)
);
}
$item = implode(',', $item);
}
return implode($this->delimiter, $val);
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue() {
// Structured text values should always be returned as a single
// array-item. Multi-value text should be returned as multiple items in
// the top-array.
if (in_array($this->name, $this->structuredValues)) {
return array($this->getParts());
} else {
return $this->getParts();
}
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "TEXT";
}
/**
* Turns the object back into a serialized blob.
*
* @return string
*/
public function serialize() {
// We need to kick in a special type of encoding, if it's a 2.1 vcard.
if ($this->root->getDocumentType() !== Document::VCARD21) {
return parent::serialize();
}
$val = $this->getParts();
if (isset($this->minimumPropertyValues[$this->name])) {
$val = array_pad($val, $this->minimumPropertyValues[$this->name], '');
}
// Imploding multiple parts into a single value, and splitting the
// values with ;.
if (count($val)>1) {
foreach($val as $k=>$v) {
$val[$k] = str_replace(';','\;', $v);
}
$val = implode(';', $val);
} else {
$val = $val[0];
}
$str = $this->name;
if ($this->group) $str = $this->group . '.' . $this->name;
foreach($this->parameters as $param) {
if ($param->getValue() === 'QUOTED-PRINTABLE') {
continue;
}
$str.=';' . $param->serialize();
}
// If the resulting value contains a \n, we must encode it as
// quoted-printable.
if (strpos($val,"\n") !== false) {
$str.=';ENCODING=QUOTED-PRINTABLE:';
$lastLine=$str;
$out = null;
// The PHP built-in quoted-printable-encode does not correctly
// encode newlines for us. Specifically, the \r\n sequence must in
// vcards be encoded as =0D=OA and we must insert soft-newlines
// every 75 bytes.
for($ii=0;$ii<strlen($val);$ii++) {
$ord = ord($val[$ii]);
// These characters are encoded as themselves.
if ($ord >= 32 && $ord <=126) {
$lastLine.=$val[$ii];
} else {
$lastLine.='=' . strtoupper(bin2hex($val[$ii]));
}
if (strlen($lastLine)>=75) {
// Soft line break
$out.=$lastLine. "=\r\n ";
$lastLine = null;
}
}
if (!is_null($lastLine)) $out.= $lastLine . "\r\n";
return $out;
} else {
$str.=':' . $val;
$out = '';
while(strlen($str)>0) {
if (strlen($str)>75) {
$out.= mb_strcut($str,0,75,'utf-8') . "\r\n";
$str = ' ' . mb_strcut($str,75,strlen($str),'utf-8');
} else {
$out.=$str . "\r\n";
$str='';
break;
}
}
return $out;
}
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* - Node::REPAIR - If something is broken, and automatic repair may
* be attempted.
*
* An array is returned with warnings.
*
* Every item in the array has the following properties:
* * level - (number between 1 and 3 with severity information)
* * message - (human readable message)
* * node - (reference to the offending node)
*
* @param int $options
* @return array
*/
public function validate($options = 0) {
$warnings = parent::validate($options);
if (isset($this->minimumPropertyValues[$this->name])) {
$minimum = $this->minimumPropertyValues[$this->name];
$parts = $this->getParts();
if (count($parts) < $minimum) {
$warnings[] = array(
'level' => 1,
'message' => 'This property must have at least ' . $minimum . ' components. It only has ' . count($parts),
'node' => $this,
);
if ($options & self::REPAIR) {
$parts = array_pad($parts, $minimum, '');
$this->setParts($parts);
}
}
}
return $warnings;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Sabre\VObject\Property;
use Sabre\VObject\DateTimeParser;
/**
* Time property
*
* This object encodes TIME values.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Time extends Text {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = null;
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "TIME";
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue() {
$parts = DateTimeParser::parseVCardTime($this->getValue());
$timeStr = '';
// Hour
if (!is_null($parts['hour'])) {
$timeStr.=$parts['hour'];
if (!is_null($parts['minute'])) {
$timeStr.=':';
}
} else {
// We know either minute or second _must_ be set, so we insert a
// dash for an empty value.
$timeStr.='-';
}
// Minute
if (!is_null($parts['minute'])) {
$timeStr.=$parts['minute'];
if (!is_null($parts['second'])) {
$timeStr.=':';
}
} else {
if (isset($parts['second'])) {
// Dash for empty minute
$timeStr.='-';
}
}
// Second
if (!is_null($parts['second'])) {
$timeStr.=$parts['second'];
}
// Timezone
if (!is_null($parts['timezone'])) {
$timeStr.=$parts['timezone'];
}
return array($timeStr);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Sabre\VObject\Property;
use
Sabre\VObject\Property,
Sabre\VObject\Component,
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\Document;
/**
* Unknown property
*
* This object represents any properties not recognized by the parser.
* This type of value has been introduced by the jCal, jCard specs.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Unknown extends Text {
/**
* 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($this->getRawMimeDirValue());
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "UNKNOWN";
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Sabre\VObject\Property;
use Sabre\VObject\Property;
/**
* URI property
*
* This object encodes URI values. vCard 2.1 calls these URL.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Uri extends Text {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = null;
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "URI";
}
/**
* 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
* @return void
*/
public function setRawMimeDirValue($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 blackslash. 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 ($this->name === 'URL') {
$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 = $val;
}
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
if (is_array($this->value)) {
return $this->value[0];
} else {
return $this->value;
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Sabre\VObject\Property;
/**
* UtcOffset property
*
* This object encodes UTC-OFFSET values.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class UtcOffset extends Text {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = null;
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "UTC-OFFSET";
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser;
/**
* Date property
*
* This object encodes vCard DATE values.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Date extends DateAndOrTime {
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "DATE";
}
/**
* Sets the property as a DateTime object.
*
* @param \DateTime $dt
* @return void
*/
public function setDateTime(\DateTime $dt) {
$this->value = $dt->format('Ymd');
}
}

View File

@@ -0,0 +1,317 @@
<?php
namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser,
Sabre\VObject\Property\Text,
Sabre\VObject\Property,
DateTime;
/**
* DateAndOrTime property
*
* This object encodes DATE-AND-OR-TIME values.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class DateAndOrTime extends Property {
/**
* Field separator
*
* @var null|string
*/
public $delimiter = null;
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "DATE-AND-OR-TIME";
}
/**
* Sets a multi-valued property.
*
* You may also specify DateTime objects here.
*
* @param array $parts
* @return void
*/
public function setParts(array $parts) {
if (count($parts)>1) {
throw new \InvalidArgumentException('Only one value allowed');
}
if (isset($parts[0]) && $parts[0] instanceof \DateTime) {
$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 DateTime here.
*
* @param string|array|\DateTime $value
* @return void
*/
public function setValue($value) {
if ($value instanceof \DateTime) {
$this->setDateTime($value);
} else {
parent::setValue($value);
}
}
/**
* Sets the property as a DateTime object.
*
* @param \DateTime $dt
* @return void
*/
public function setDateTime(\DateTime $dt) {
$values = array();
$tz = null;
$isUtc = false;
$tz = $dt->getTimeZone();
$isUtc = in_array($tz->getName() , array('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 \DateTime
*/
public function getDateTime() {
$dts = array();
$now = new DateTime();
$tzFormat = $now->getTimezone()->getOffset($now)===0?'\\Z':'O';
$nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This' . $tzFormat));
$value = $this->getValue();
$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 DateTime("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]");
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue() {
$parts = DateTimeParser::parseVCardDateTime($this->getValue());
$dateStr = '';
// Year
if (!is_null($parts['year'])) {
$dateStr.=$parts['year'];
if (!is_null($parts['month'])) {
// If a year and a month is set, we need to insert a separator
// dash.
$dateStr.='-';
}
} else {
if (!is_null($parts['month']) || !is_null($parts['date'])) {
// Inserting two dashes
$dateStr.='--';
}
}
// Month
if (!is_null($parts['month'])) {
$dateStr.=$parts['month'];
if (isset($parts['date'])) {
// If month and date are set, we need the separator dash.
$dateStr.='-';
}
} else {
if (isset($parts['date'])) {
// If the month is empty, and a date is set, we need a 'empty
// dash'
$dateStr.='-';
}
}
// Date
if (!is_null($parts['date'])) {
$dateStr.=$parts['date'];
}
// Early exit if we don't have a time string.
if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) {
return array($dateStr);
}
$dateStr.='T';
// Hour
if (!is_null($parts['hour'])) {
$dateStr.=$parts['hour'];
if (!is_null($parts['minute'])) {
$dateStr.=':';
}
} else {
// We know either minute or second _must_ be set, so we insert a
// dash for an empty value.
$dateStr.='-';
}
// Minute
if (!is_null($parts['minute'])) {
$dateStr.=$parts['minute'];
if (!is_null($parts['second'])) {
$dateStr.=':';
}
} else {
if (isset($parts['second'])) {
// Dash for empty minute
$dateStr.='-';
}
}
// Second
if (!is_null($parts['second'])) {
$dateStr.=$parts['second'];
}
// Timezone
if (!is_null($parts['timezone'])) {
$dateStr.=$parts['timezone'];
}
return array($dateStr);
}
/**
* 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
* @return void
*/
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 (\InvalidArgumentException $e) {
$messages[] = array(
'level' => 3,
'message' => 'The supplied value (' . $value . ') is not a correct DATE-AND-OR-TIME property',
'node' => $this,
);
}
return $messages;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser;
/**
* DateTime property
*
* This object encodes DATE-TIME values for vCards.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class DateTime extends DateAndOrTime {
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "DATE-TIME";
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\Property;
/**
* LanguageTag property
*
* This object represents LANGUAGE-TAG values as used in vCards.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class LanguageTag extends Property {
/**
* 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
* @return void
*/
public function setRawMimeDirValue($val) {
$this->setValue($val);
}
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
public function getRawMimeDirValue() {
return $this->getValue();
}
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "LANGUAGE-TAG";
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser,
Sabre\VObject\Property\Text;
/**
* TimeStamp property
*
* This object encodes TIMESTAMP values.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class TimeStamp extends Text {
/**
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var string|null
*/
public $delimiter = null;
/**
* Returns the type of value.
*
* This corresponds to the VALUE= parameter. Every property also has a
* 'default' valueType.
*
* @return string
*/
public function getValueType() {
return "TIMESTAMP";
}
/**
* Returns the value, in the format it should be encoded for json.
*
* This method must always return an array.
*
* @return array
*/
public function getJsonValue() {
$parts = DateTimeParser::parseVCardDateTime($this->getValue());
$dateStr =
$parts['year'] . '-' .
$parts['month'] . '-' .
$parts['date'] . 'T' .
$parts['hour'] . ':' .
$parts['minute'] . ':' .
$parts['second'];
// Timezone
if (!is_null($parts['timezone'])) {
$dateStr.=$parts['timezone'];
}
return array($dateStr);
}
}

73
vendor/sabre/vobject/lib/Reader.php vendored Normal file
View File

@@ -0,0 +1,73 @@
<?php
namespace Sabre\VObject;
/**
* iCalendar/vCard/jCal/jCard reader object.
*
* This object provides a few (static) convenience methods to quickly access
* the parsers.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Reader {
/**
* If this option is passed to the reader, it will be less strict about the
* validity of the lines.
*/
const OPTION_FORGIVING = 1;
/**
* If this option is turned on, any lines we cannot parse will be ignored
* by the reader.
*/
const OPTION_IGNORE_INVALID_LINES = 2;
/**
* Parses a vCard or iCalendar 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
*/
static public function read($data, $options = 0) {
$parser = new Parser\MimeDir();
$result = $parser->parse($data, $options);
return $result;
}
/**
* Parses a jCard or jCal 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 a string, a readable stream, or an array for it's input.
* Specifying the array is useful if json_decode was already called on the
* input.
*
* @param string|resource|array $data
* @param int $options
* @return Node
*/
static public function readJson($data, $options = 0) {
$parser = new Parser\Json();
$result = $parser->parse($data, $options);
return $result;
}
}

View File

@@ -0,0 +1,500 @@
<?php
namespace Sabre\VObject\Recur;
use InvalidArgumentException;
use DateTime;
use DateTimeZone;
use Sabre\VObject\Component;
use Sabre\VObject\Component\VEvent;
/**
* 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
* * 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 recomended. 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($this->timeZone)) {
$timeZone = new DateTimeZone('UTC');
}
$this->timeZone = $timeZone;
if (is_array($input)) {
$events = $input;
} elseif ($input instanceof VEvent) {
// Single instance mode.
$events = array($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->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(
array(
'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 DateTime
*/
public function current() {
if ($this->currentDate) {
return clone $this->currentDate;
}
}
/**
* This method returns the start date for the current iteration of the
* event.
*
* @return DateTime
*/
public function getDtStart() {
if ($this->currentDate) {
return clone $this->currentDate;
}
}
/**
* This method returns the end date for the current iteration of the
* event.
*
* @return DateTime
*/
public function getDtEnd() {
if (!$this->valid()) {
return null;
}
$end = clone $this->currentDate;
$end->modify('+' . $this->eventDuration . ' seconds');
return $end;
}
/**
* Returns a VEVENT for the current iterations of the event.
*
* This VEVENT will have a recurrence id, and it's 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
*/
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
*/
public function valid() {
return !!$this->currentDate;
}
/**
* Sets the iterator back to the starting point.
*/
public function rewind() {
$this->recurIterator->rewind();
// re-creating overridden event index.
$index = array();
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
*/
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.
*
* @param DateTime $dateTime
*/
public function fastForward(DateTime $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 = array();
/**
* 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 = array();
/**
* Internal event counter
*
* @var int
*/
protected $counter;
/**
* The very start of the iteration process.
*
* @var DateTime
*/
protected $startDate;
/**
* Where we are currently in the iteration process
*
* @var DateTime
*/
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 DateTime
*/
protected $nextDate;
}

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 occurence 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,174 @@
<?php
namespace Sabre\VObject\Recur;
use DateTime;
use InvalidArgumentException;
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
* @param DateTime $start
*/
public function __construct($rrule, DateTime $start) {
$this->startDate = $start;
$this->parseRDate($rrule);
$this->currentDate = clone $this->startDate;
}
/* Implementation of the Iterator interface {{{ */
public function current() {
if (!$this->valid()) return null;
return clone $this->currentDate;
}
/**
* Returns the current item number.
*
* @return int
*/
public function key() {
return $this->counter;
}
/**
* Returns whether the current item is a valid item for the recurrence
* iterator.
*
* @return bool
*/
public function valid() {
return ($this->counter <= count($this->dates));
}
/**
* Resets the iterator.
*
* @return void
*/
public function rewind() {
$this->currentDate = clone $this->startDate;
$this->counter = 0;
}
/**
* Goes on to the next iteration.
*
* @return void
*/
public function next() {
$this->counter++;
if (!$this->valid()) return;
$this->currentDate =
DateTimeParser::parse(
$this->dates[$this->counter-1]
);
}
/* 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.
*
* @param DateTime $dt
* @return void
*/
public function fastForward(\DateTime $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 DateTime
*/
protected $startDate;
/**
* The date of the current iteration. You can get this by calling
* ->current().
*
* @var DateTime
*/
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
* @return void
*/
protected function parseRDate($rdate) {
if (is_string($rdate)) {
$rdate = explode(',', $rdate);
}
$this->dates = $rdate;
}
}

View File

@@ -0,0 +1,904 @@
<?php
namespace Sabre\VObject\Recur;
use DateTime;
use InvalidArgumentException;
use Iterator;
use Sabre\VObject\DateTimeParser;
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
* @param DateTime $start
*/
public function __construct($rrule, DateTime $start) {
$this->startDate = $start;
$this->parseRRule($rrule);
$this->currentDate = clone $this->startDate;
}
/* Implementation of the Iterator interface {{{ */
public function current() {
if (!$this->valid()) return null;
return clone $this->currentDate;
}
/**
* Returns the current item number
*
* @return int
*/
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
*/
public function valid() {
if (!is_null($this->count)) {
return $this->counter < $this->count;
}
return is_null($this->until) || $this->currentDate <= $this->until;
}
/**
* Resets the iterator
*
* @return void
*/
public function rewind() {
$this->currentDate = clone $this->startDate;
$this->counter = 0;
}
/**
* Goes on to the next iteration
*
* @return void
*/
public function next() {
$previousStamp = $this->currentDate->getTimeStamp();
// 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.
*
* @param DateTime $dt
* @return void
*/
public function fastForward(\DateTime $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 DateTime
*/
protected $startDate;
/**
* The date of the current iteration. You can get this by calling
* ->current().
*
* @var DateTime
*/
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 \DateTime|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 preceeded 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.
*
* @return void
*/
protected function nextHourly() {
$this->currentDate->modify('+' . $this->interval . ' hours');
}
/**
* Does the processing for advancing the iterator for daily frequency.
*
* @return void
*/
protected function nextDaily() {
if (!$this->byHour && !$this->byDay) {
$this->currentDate->modify('+' . $this->interval . ' days');
return;
}
if (isset($this->byHour)) {
$recurrenceHours = $this->getHours();
}
if (isset($this->byDay)) {
$recurrenceDays = $this->getDays();
}
if (isset($this->byMonth)) {
$recurrenceMonths = $this->getMonths();
}
do {
if ($this->byHour) {
if ($this->currentDate->format('G') == '23') {
// to obey the interval rule
$this->currentDate->modify('+' . $this->interval-1 . ' days');
}
$this->currentDate->modify('+1 hours');
} else {
$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.
*
* @return void
*/
protected function nextWeekly() {
if (!$this->byHour && !$this->byDay) {
$this->currentDate->modify('+' . $this->interval . ' weeks');
return;
}
if ($this->byHour) {
$recurrenceHours = $this->getHours();
}
if ($this->byDay) {
$recurrenceDays = $this->getDays();
}
// First day of the week:
$firstDay = $this->dayMap[$this->weekStart];
do {
if ($this->byHour) {
$this->currentDate->modify('+1 hours');
} else {
$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 || $currentHour == '0')) {
$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->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.
*
* @return void
*/
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->modify('+' . $this->interval . ' months');
} else {
$increase = 0;
do {
$increase++;
$tempDate = clone $this->currentDate;
$tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
} while ($tempDate->format('j') != $currentDayOfMonth);
$this->currentDate = $tempDate;
}
return;
}
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 \DateTime($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone());
// end of workaround
$this->currentDate->modify('+ ' . $this->interval . ' months');
// This goes to 0 because we need to start counting at the
// beginning.
$currentDayOfMonth = 0;
}
$this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
}
/**
* Does the processing for advancing the iterator for yearly frequency.
*
* @return void
*/
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 (!$this->byMonth) {
// Unless it was a leap day!
if ($currentMonth==2 && $currentDayOfMonth==29) {
$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->modify('+ ' . ($this->interval*$counter) . ' years');
} while ($nextDate->format('n')!=2);
$this->currentDate = $nextDate;
return;
}
// The easiest form
$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) {
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->setDate($currentYear, $currentMonth, $currentDayOfMonth);
}
// If we made it here, it means we got a valid occurrence
$this->currentDate->setDate($currentYear, $currentMonth, $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->setDate($currentYear, $currentMonth, $currentDayOfMonth);
return;
}
}
/* }}} */
/**
* This method receives a string from an RRULE property, and populates this
* class with all the values.
*
* @param string|array $rrule
* @return void
*/
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,
array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
)) {
throw new InvalidArgumentException('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' :
// No break
case 'COUNT' :
$val = (int)$value;
if ($val < 1) {
throw new \InvalidArgumentException(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 \InvalidArgumentException('Invalid part in BYDAY clause: ' . $part);
}
}
$this->byDay = $value;
break;
case 'BYMONTHDAY' :
$this->byMonthDay = (array)$value;
break;
case 'BYYEARDAY' :
$this->byYearDay = (array)$value;
break;
case 'BYWEEKNO' :
$this->byWeekNo = (array)$value;
break;
case 'BYMONTH' :
$this->byMonth = (array)$value;
break;
case 'BYSETPOS' :
$this->bySetPos = (array)$value;
break;
case 'WKST' :
$this->weekStart = strtoupper($value);
break;
default:
throw new \InvalidArgumentException('Not supported: ' . strtoupper($key));
}
}
}
/**
* Mappings between the day number and english day name.
*
* @var array
*/
protected $dayNames = array(
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 = array();
// 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 = array();
// 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->modify($dayName);
}
do {
$dayHits[] = $checkDate->format('j');
$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 = array();
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 = array();
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 = array(
'SU' => 0,
'MO' => 1,
'TU' => 2,
'WE' => 3,
'TH' => 4,
'FR' => 5,
'SA' => 6,
);
protected function getHours()
{
$recurrenceHours = array();
foreach($this->byHour as $byHour) {
$recurrenceHours[] = $byHour;
}
return $recurrenceHours;
}
protected function getDays() {
$recurrenceDays = array();
foreach($this->byDay as $byDay) {
// The day may be preceeded 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 = array();
foreach($this->byMonth as $byMonth) {
$recurrenceMonths[] = $byMonth;
}
return $recurrenceMonths;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Sabre\VObject;
use Sabre\VObject\Recur\EventIterator;
/**
* RecurrenceIterator
*
* This class is deprecated. Use Sabre\VObject\Recur\EventIterator instead.
* This class will be removed from a future version.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @deprecated
* @license http://sabre.io/license Modified BSD License
*/
class RecurrenceIterator extends EventIterator {
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Sabre\VObject\Splitter;
use
Sabre\VObject,
Sabre\VObject\Component\VCalendar;
/**
* Splitter
*
* This class is responsible for splitting up iCalendar objects.
*
* This class expects a single VCALENDAR object with one or more
* calendar-objects inside. Objects with identical UID's will be combined into
* a single object.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Dominik Tobschall
* @author Armin Hackmann
* @license http://sabre.io/license/ Modified BSD License
*/
class ICalendar implements SplitterInterface {
/**
* Timezones
*
* @var array
*/
protected $vtimezones = array();
/**
* iCalendar objects
*
* @var array
*/
protected $objects = array();
/**
* Constructor
*
* The splitter should receive an readable file stream as it's input.
*
* @param resource $input
* @param int $options Parser options, see the OPTIONS constants.
*/
public function __construct($input, $options = 0) {
$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) {
if (!$component instanceof VObject\Component) {
continue;
}
// Get all timezones
if ($component->name === 'VTIMEZONE') {
$this->vtimezones[(string)$component->TZID] = $component;
continue;
}
// Get component UID for recurring Events search
if(!$component->UID) {
$component->UID = sha1(microtime()) . '-vobjectimport';
}
$uid = (string)$component->UID;
// Take care of recurring events
if (!array_key_exists($uid, $this->objects)) {
$this->objects[$uid] = new VCalendar();
}
$this->objects[$uid]->add(clone $component);
}
}
/**
* Every time getNext() is called, a new object will be parsed, until we
* hit the end of the stream.
*
* When the end is reached, null will be returned.
*
* @return Sabre\VObject\Component|null
*/
public function getNext() {
if($object=array_shift($this->objects)) {
// create our baseobject
$object->version = '2.0';
$object->prodid = '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
$object->calscale = 'GREGORIAN';
// add vtimezone information to obj (if we have it)
foreach ($this->vtimezones as $vtimezone) {
$object->add($vtimezone);
}
return $object;
} else {
return null;
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Sabre\VObject\Splitter;
/**
* VObject splitter
*
* The splitter is responsible for reading a large vCard or iCalendar object,
* and splitting it into multiple objects.
*
* 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.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Dominik Tobschall
* @license http://sabre.io/license/ Modified BSD License
*/
interface SplitterInterface {
/**
* Constructor
*
* The splitter should receive an readable file stream as it's input.
*
* @param resource $input
*/
public function __construct($input);
/**
* Every time getNext() is called, a new object will be parsed, until we
* hit the end of the stream.
*
* When the end is reached, null will be returned.
*
* @return Sabre\VObject\Component|null
*/
public function getNext();
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Sabre\VObject\Splitter;
use
Sabre\VObject,
Sabre\VObject\Parser\MimeDir;
/**
* Splitter
*
* This class is responsible for splitting up VCard objects.
*
* It is assumed that the input stream contains 1 or more VCARD objects. This
* class checks for BEGIN:VCARD and END:VCARD and parses each encountered
* component individually.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Dominik Tobschall
* @author Armin Hackmann
* @license http://sabre.io/license/ Modified BSD License
*/
class VCard implements SplitterInterface {
/**
* File handle
*
* @var resource
*/
protected $input;
/**
* Persistent parser
*
* @var MimeDir
*/
protected $parser;
/**
* Constructor
*
* The splitter should receive an readable file stream as it's input.
*
* @param resource $input
* @param int $options Parser options, see the OPTIONS constants.
*/
public function __construct($input, $options = 0) {
$this->input = $input;
$this->parser = new MimeDir($input, $options);
}
/**
* Every time getNext() is called, a new object will be parsed, until we
* hit the end of the stream.
*
* When the end is reached, null will be returned.
*
* @return Sabre\VObject\Component|null
*/
public function getNext() {
try {
$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) {
return null;
}
return $object;
}
}

65
vendor/sabre/vobject/lib/StringUtil.php vendored Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace Sabre\VObject;
/**
* Useful utilities for working with various strings.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class StringUtil {
/**
* Returns true or false depending on if a string is valid UTF-8
*
* @param string $str
* @return bool
*/
static public function isUTF8($str) {
// Control characters
if (preg_match('%[\x00-\x08\x0B-\x0C\x0E\x0F]%', $str)) {
return false;
}
return (bool)preg_match('%%u', $str);
}
/**
* This method tries its best to convert the input string to UTF-8.
*
* Currently only ISO-5991-1 input and UTF-8 input is supported, but this
* may be expanded upon if we receive other examples.
*
* @param string $str
* @return string
*/
static public function convertToUTF8($str) {
$encoding = mb_detect_encoding($str , array('UTF-8','ISO-8859-1', 'WINDOWS-1252'), true);
switch($encoding) {
case 'ISO-8859-1' :
$newStr = utf8_encode($str);
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;
}
// Removing any control characters
return (preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', '', $newStr));
}
}

View File

@@ -0,0 +1,265 @@
<?php
namespace Sabre\VObject;
/**
* Time zone name translation
*
* This file translates well-known time zone names into "Olson database" time zone names.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Frank Edelhaeuser (fedel@users.sourceforge.net)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class TimeZoneUtil {
public static $map = null;
/**
* List of microsoft exchange timezone ids.
*
* Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
*/
public static $microsoftExchangeMap = array(
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',
);
/**
* This method will try to find out the correct timezone for an iCalendar
* date-time value.
*
* You must pass the contents of the TZID parameter, as well as the full
* 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) {
// 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 ($tzid[0]!=='(') {
// 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 successfull 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, self::getIdentifiersBC()))
) {
return new \DateTimeZone($tzid);
}
} catch(\Exception $e) {
}
}
self::loadTzMaps();
// Next, we check if the tzid is somewhere in our tzid map.
if (isset(self::$map[$tzid])) {
return new \DateTimeZone(self::$map[$tzid]);
}
// 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
}
if ($vcalendar) {
// If that didn't work, we will scan VTIMEZONE objects
foreach($vcalendar->select('VTIMEZONE') as $vtimezone) {
if ((string)$vtimezone->TZID === $tzid) {
// Some clients add 'X-LIC-LOCATION' with the olson name.
if (isset($vtimezone->{'X-LIC-LOCATION'})) {
$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);
}
return self::getTimeZone($lic, null, $failIfUncertain);
}
// 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());
}
/**
* This method will load in all the tz mapping information, if it's not yet
* done.
*/
static public function loadTzMaps() {
if (!is_null(self::$map)) return;
self::$map = array_merge(
include __DIR__ . '/timezonedata/windowszones.php',
include __DIR__ . '/timezonedata/lotuszones.php',
include __DIR__ . '/timezonedata/exchangezones.php',
include __DIR__ . '/timezonedata/php-workaround.php'
);
}
/**
* 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
*/
static public function getIdentifiersBC() {
return include __DIR__ . '/timezonedata/php-bc.php';
}
}

67
vendor/sabre/vobject/lib/UUIDUtil.php vendored Normal file
View File

@@ -0,0 +1,67 @@
<?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
*/
static public 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
*/
static public function validateUUID($uuid) {
return preg_match(
'/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i',
$uuid
) == true;
}
}

View File

@@ -0,0 +1,459 @@
<?php
namespace Sabre\VObject;
/**
* This utility converts vcards from one version to another.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VCardConverter {
/**
* Converts a vCard object to a new version.
*
* targetVersion must be one of:
* Document::VCARD21
* Document::VCARD30
* Document::VCARD40
*
* Currently only 3.0 and 4.0 as input and output versions.
*
* 2.1 has some minor support for the input version, it's incomplete at the
* moment though.
*
* If input and output version are identical, a clone is returned.
*
* @param Component\VCard $input
* @param int $targetVersion
*/
public function convert(Component\VCard $input, $targetVersion) {
$inputVersion = $input->getDocumentType();
if ($inputVersion===$targetVersion) {
return clone $input;
}
if (!in_array($inputVersion, array(Document::VCARD21, Document::VCARD30, Document::VCARD40))) {
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))) {
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';
$output = new Component\VCard(array(
'VERSION' => $newVersion,
));
foreach($input->children as $property) {
$this->convertProperty($input, $output, $property, $targetVersion);
}
return $output;
}
/**
* Handles conversion of a single property.
*
* @param Component\VCard $input
* @param Component\VCard $output
* @param Property $property
* @param int $targetVersion
* @return void
*/
protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) {
// Skipping these, those are automatically added.
if (in_array($property->name, array('VERSION', 'PRODID'))) {
return;
}
$parameters = $property->parameters();
$valueType = null;
if (isset($parameters['VALUE'])) {
$valueType = $parameters['VALUE']->getValue();
unset($parameters['VALUE']);
}
if (!$valueType) {
$valueType = $property->getValueType();
}
$newProperty = $output->createProperty(
$property->name,
$property->getParts(),
array(), // parameters will get added a bit later.
$valueType
);
if ($targetVersion===Document::VCARD30) {
if ($property instanceof Property\Uri && in_array($property->name, array('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 ($newProperty->name == 'ANNIVERSARY') {
// Microsoft non-standard anniversary
$newProperty->name = 'X-ANNIVERSARY';
// 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(), array('VALUE' => 'DATE-AND-OR-TIME'));
$output->add('ITEM' . $x . '.X-ABLABEL', '_$!<Anniversary>!$_');
}
} elseif ($property->name === 'KIND') {
switch(strtolower($property->getValue())) {
case 'org' :
// 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');
break;
case 'individual' :
// Individual is implicit, so we skip it.
return;
case 'group' :
// OS X addressbook property
$newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND','GROUP');
break;
}
}
} elseif ($targetVersion===Document::VCARD40) {
// These properties were removed in vCard 4.0
if (in_array($property->name, array('NAME', 'MAILER', 'LABEL', 'CLASS'))) {
return;
}
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);
}
// Regardless if the year matched or not, we do need to strip
// X-APPLE-OMIT-YEAR.
unset($parameters['X-APPLE-OMIT-YEAR']);
}
switch($property->name) {
case 'X-ABSHOWAS' :
if (strtoupper($property->getValue()) === 'COMPANY') {
$newProperty = $output->createProperty('KIND','ORG');
}
break;
case 'X-ADDRESSBOOKSERVER-KIND' :
if (strtoupper($property->getValue()) === 'GROUP') {
$newProperty = $output->createProperty('KIND','GROUP');
}
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 || $label->getValue()!=='_$!<Anniversary>!$_') {
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;
// Apple's per-property label system.
case 'X-ABLABEL' :
if($newProperty->getValue() === '_$!<Anniversary>!$_') {
// We can safely remove these, as they are converted to
// ANNIVERSARY properties.
return;
}
break;
}
}
// set property group
$newProperty->group = $property->group;
if ($targetVersion===Document::VCARD40) {
$this->convertParameters40($newProperty, $parameters);
} else {
$this->convertParameters30($newProperty, $parameters);
}
// 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
// seeing if the default valueType is identical to the current one.
$tempProperty = $output->createProperty($newProperty->name);
if ($tempProperty->getValueType() !== $newProperty->getValueType()) {
$newProperty['VALUE'] = $newProperty->getValueType();
}
$output->add($newProperty);
}
/**
* Converts a BINARY property to a URI property.
*
* vCard 4.0 no longer supports BINARY properties.
*
* @param Component\VCard $output
* @param Property\Uri $property The input property.
* @param $parameters List of parameters that will eventually be added to
* the new property.
* @return Property\Uri
*/
protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters) {
$value = $newProperty->getValue();
$newProperty = $output->createProperty(
$newProperty->name,
null, // no value
array(), // no parameters yet
'URI' // Forcing the BINARY type
);
$mimeType = 'application/octet-stream';
// See if we can find a better mimetype.
if (isset($parameters['TYPE'])) {
$newTypes = array();
foreach($parameters['TYPE']->getParts() as $typePart) {
if (in_array(
strtoupper($typePart),
array('JPEG','PNG','GIF')
)) {
$mimeType = 'image/' . strtolower($typePart);
} else {
$newTypes[] = $typePart;
}
}
// If there were any parameters we're not converting to a
// mime-type, we need to keep them.
if ($newTypes) {
$parameters['TYPE']->setParts($newTypes);
} else {
unset($parameters['TYPE']);
}
}
$newProperty->setValue('data:' . $mimeType . ';base64,' . base64_encode($value));
return $newProperty;
}
/**
* Converts a URI property to a BINARY property.
*
* In vCard 4.0 attachments are encoded as data: uri. Even though these may
* be valid in vCard 3.0 as well, we should convert those to BINARY if
* possible, to improve compatibility.
*
* @param Component\VCard $output
* @param Property\Uri $property The input property.
* @return Property\Binary|null
*/
protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty) {
$value = $newProperty->getValue();
// Only converting data: uris
if (substr($value, 0, 5)!=='data:') {
return $newProperty;
}
$newProperty = $output->createProperty(
$newProperty->name,
null, // no value
array(), // no parameters yet
'BINARY'
);
$mimeType = substr($value, 5, strpos($value, ',')-5);
if (strpos($mimeType, ';')) {
$mimeType = substr($mimeType,0,strpos($mimeType, ';'));
$newProperty->setValue(base64_decode(substr($value, strpos($value,',')+1)));
} else {
$newProperty->setValue(substr($value, strpos($value,',')+1));
}
unset($value);
$newProperty['ENCODING'] = 'b';
switch($mimeType) {
case 'image/jpeg' :
$newProperty['TYPE'] = 'JPEG';
break;
case 'image/png' :
$newProperty['TYPE'] = 'PNG';
break;
case 'image/gif' :
$newProperty['TYPE'] = 'GIF';
break;
}
return $newProperty;
}
/**
* 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) {
// Adding all parameters.
foreach($parameters as $param) {
// vCard 2.1 allowed parameters with no name
if ($param->noName) $param->noName = false;
switch($param->name) {
// We need to see if there's any TYPE=PREF, because in vCard 4
// that's now PREF=1.
case 'TYPE' :
foreach($param->getParts() as $paramPart) {
if (strtoupper($paramPart)==='PREF') {
$newProperty->add('PREF','1');
} else {
$newProperty->add($param->name, $paramPart);
}
}
break;
// These no longer exist in vCard 4
case 'ENCODING' :
case 'CHARSET' :
break;
default :
$newProperty->add($param->name, $param->getParts());
break;
}
}
}
/**
* 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) {
// Adding all parameters.
foreach($parameters as $param) {
// vCard 2.1 allowed parameters with no name
if ($param->noName) $param->noName = false;
switch($param->name) {
case 'ENCODING' :
// This value only existed in vCard 2.1, and should be
// removed for anything else.
if (strtoupper($param->getValue())!=='QUOTED-PRINTABLE') {
$newProperty->add($param->name, $param->getParts());
}
break;
/*
* Converting PREF=1 to TYPE=PREF.
*
* Any other PREF numbers we'll drop.
*/
case 'PREF' :
if ($param->getValue()=='1') {
$newProperty->add('TYPE','PREF');
}
break;
default :
$newProperty->add($param->name, $param->getParts());
break;
}
}
}
}

19
vendor/sabre/vobject/lib/Version.php vendored Normal file
View File

@@ -0,0 +1,19 @@
<?php
namespace Sabre\VObject;
/**
* This class contains the version number for the VObject package
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Version {
/**
* Full version number
*/
const VERSION = '3.5.3';
}

View File

@@ -0,0 +1,93 @@
<?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 array(
'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',
);

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 array(
'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,153 @@
<?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 array(
'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/East-Saskatchewan',
'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/Pacific-New',
'US/Samoa',
'WET',
);

View File

@@ -0,0 +1,45 @@
<?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 array(
'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,118 @@
<?php
/**
* Automatically generated timezone file
*
* Last update: 2015-01-30T15:01:05-05:00
* Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
return array (
'AUS Central Standard Time' => 'Australia/Darwin',
'AUS Eastern Standard Time' => 'Australia/Sydney',
'Afghanistan Standard Time' => 'Asia/Kabul',
'Alaskan Standard Time' => 'America/Anchorage',
'Arab Standard Time' => 'Asia/Riyadh',
'Arabian Standard Time' => 'Asia/Dubai',
'Arabic Standard Time' => 'Asia/Baghdad',
'Argentina Standard Time' => 'America/Buenos_Aires',
'Atlantic Standard Time' => 'America/Halifax',
'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',
'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. 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/Bucharest',
'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',
'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
'Korea Standard Time' => 'Asia/Seoul',
'Libya Standard Time' => 'Africa/Tripoli',
'Line Islands Standard Time' => 'Pacific/Kiritimati',
'Magadan Standard Time' => 'Asia/Magadan',
'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',
'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',
'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',
'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',
'Turkey Standard Time' => 'Europe/Istanbul',
'US Eastern Standard Time' => 'America/Indianapolis',
'US Mountain Standard Time' => 'America/Phoenix',
'UTC' => 'Etc/GMT',
'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',
);