CardDAV (pre-alpha/unstable)

This commit is contained in:
RainLoop Team 2013-12-23 04:06:48 +04:00
parent 0ec965bccb
commit 599e934b4a
76 changed files with 6984 additions and 1512 deletions

View file

@ -0,0 +1,735 @@
<?php
namespace Sabre\VObject;
use
InvalidArgumentException;
/**
* This is the CLI interface for sabre-vobject.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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 = STDERR;
}
if (!$this->stdout) {
$this->stdout = STDOUT;
}
if (!$this->stdin) {
$this->stdin = STDIN;
}
// @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 {
$returnCode = 2;
foreach($warnings as $warn) {
$this->log(" " . $warn['message']);
}
}
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 {
foreach($warnings as $warn) {
$returnCode = 2;
$this->log(" " . $warn['message']);
}
}
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");
}
}
}

View file

@ -3,82 +3,218 @@
namespace Sabre\VObject;
/**
* VObject Component
* Component
*
* This class represents a VCALENDAR/VCARD component. A component is for example
* VEVENT, VTODO and also VCALENDAR. It starts with BEGIN:COMPONENTNAME and
* ends with END:COMPONENTNAME
* A component represents a group of properties, such as VCALENDAR, VEVENT, or
* VCARD.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Component extends Node {
/**
* Name, for example VEVENT
* Component name.
*
* This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD.
*
* @var string
*/
public $name;
/**
* Children properties and components
* A list of properties and/or sub-components.
*
* @var array
*/
public $children = array();
/**
* If components are added to this map, they will be automatically mapped
* to their respective classes, if parsed by the reader or constructed with
* the 'create' method.
* Creates a new component.
*
* @var array
*/
static public $classMap = array(
'VALARM' => 'Sabre\\VObject\\Component\\VAlarm',
'VCALENDAR' => 'Sabre\\VObject\\Component\\VCalendar',
'VCARD' => 'Sabre\\VObject\\Component\\VCard',
'VEVENT' => 'Sabre\\VObject\\Component\\VEvent',
'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal',
'VTODO' => 'Sabre\\VObject\\Component\\VTodo',
'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy',
);
/**
* Creates the new component by name, but in addition will also see if
* there's a class mapped to the property 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.
*
* @param string $name
* @param string $value
* @return Component
* 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
*/
static public function create($name, $value = null) {
public function __construct(Document $root, $name, array $children = array(), $defaults = true) {
$name = strtoupper($name);
$this->name = strtoupper($name);
$this->root = $root;
if (isset(self::$classMap[$name])) {
return new self::$classMap[$name]($name, $value);
} else {
return new self($name, $value);
if ($defaults) {
$children = array_merge($this->getDefaults(), $children);
}
foreach($children as $k=>$child) {
if ($child instanceof Node) {
// Component or Property
$this->add($child);
} else {
// Property key=>value
$this->add($k, $child);
}
}
}
/**
* Creates a new component.
* Adds a new property or component, and returns the new item.
*
* By default this object will iterate over its own children, but this can
* be overridden with the iterator argument
* 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
*/
public 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
*/
public 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
*/
public function children() {
return $this->children;
}
/**
* This method only returns a list of sub-components. Properties are
* ignored.
*
* @return array
*/
public 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
* @param ElementList $iterator
* @return array
*/
public function __construct($name, ElementList $iterator = null) {
public function select($name) {
$this->name = strtoupper($name);
if (!is_null($iterator)) $this->iterator = $iterator;
$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))
) {
$result[$key] = $child;
}
}
reset($result);
return $result;
}
@ -141,9 +277,7 @@ class Component extends Node {
$sA = $sortScore($a, $tmp);
$sB = $sortScore($b, $tmp);
if ($sA === $sB) return 0;
return ($sA < $sB) ? -1 : 1;
return $sA - $sB;
});
@ -155,149 +289,55 @@ class Component extends Node {
}
/**
* Adds a new component or element
*
* You can call this method with the following syntaxes:
*
* add(Node $node)
* add(string $name, $value, array $parameters = array())
*
* The first version adds an Element
* The second adds a property as a string.
*
* @param mixed $item
* @param mixed $itemValue
* @return void
*/
public function add($item, $itemValue = null, array $parameters = array()) {
if ($item instanceof Node) {
if (!is_null($itemValue)) {
throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
}
$item->parent = $this;
$this->children[] = $item;
} elseif(is_string($item)) {
$item = Property::create($item,$itemValue, $parameters);
$item->parent = $this;
$this->children[] = $item;
} else {
throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
}
}
/**
* Returns an iterable list of children
*
* @return ElementList
*/
public function children() {
return new ElementList($this->children);
}
/**
* 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
*/
public 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))
) {
$result[$key] = $child;
}
}
reset($result);
return $result;
}
/**
* This method only returns a list of sub-components. Properties are
* ignored.
* 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 getComponents() {
public function jsonSerialize() {
$components = array();
$properties = array();
$result = array();
foreach($this->children as $child) {
if ($child instanceof Component) {
$result[] = $child;
$components[] = $child->jsonSerialize();
} else {
$properties[] = $child->jsonSerialize();
}
}
return $result;
return array(
strtolower($this->name),
$properties,
$components
);
}
/**
* Validates the node for correctness.
* This method should return a list of default property values.
*
* 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) {
protected function getDefaults() {
$result = array();
foreach($this->children as $child) {
$result = array_merge($result, $child->validate($options));
}
return $result;
return array();
}
/* Magic property accessors {{{ */
/**
* Using 'get' you will either get a property or component,
* 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
*/
@ -353,8 +393,8 @@ class Component extends Node {
} else {
$this->children[] = $value;
}
} elseif (is_scalar($value)) {
$property = Property::create($name,$value);
} elseif (is_scalar($value) || is_array($value) || is_null($value)) {
$property = $this->root->create($name,$value);
$property->parent = $this;
if (!is_null($overWrite)) {
$this->children[$overWrite] = $property;
@ -368,7 +408,8 @@ class Component extends Node {
}
/**
* Removes all properties and components within this component.
* Removes all properties and components within this component with the
* specified name.
*
* @param string $name
* @return void
@ -402,4 +443,31 @@ class Component extends Node {
}
/**
* Validates the node for correctness.
*
* The following options are supported:
* - Node::REPAIR - If something is broken, an 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) {
$result = array();
foreach($this->children as $child) {
$result = array_merge($result, $child->validate($options));
}
return $result;
}
}

View file

@ -2,7 +2,9 @@
namespace Sabre\VObject\Component;
use Sabre\VObject;
use
Sabre\VObject,
Sabre\VObject\Component;
/**
* The VCalendar component
@ -15,7 +17,140 @@ use Sabre\VObject;
*/
class VCalendar extends VObject\Document {
static $defaultName = 'VCALENDAR';
/**
* The default name for this component.
*
* This should be 'VCALENDAR' or 'VCARD'.
*
* @var string
*/
static public $defaultName = 'VCALENDAR';
/**
* This is a list of components, and which classes they should map to.
*
* @var array
*/
static public $componentMap = array(
'VEVENT' => 'Sabre\\VObject\\Component\\VEvent',
'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy',
'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal',
'VTODO' => 'Sabre\\VObject\\Component\\VTodo',
'VALARM' => 'Sabre\\VObject\\Component\\VAlarm',
);
/**
* List of value-types, and which classes they map to.
*
* @var array
*/
static public $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\\Float',
'INTEGER' => 'Sabre\\VObject\\Property\\Integer',
'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 public $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\\Binary',
'CATEGORIES' => 'Sabre\\VObject\\Property\\Text',
'CLASS' => 'Sabre\\VObject\\Property\\FlatText',
'COMMENT' => 'Sabre\\VObject\\Property\\FlatText',
'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText',
'GEO' => 'Sabre\\VObject\\Property\\Float',
'LOCATION' => 'Sabre\\VObject\\Property\\FlatText',
'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\Integer',
'PRIORITY' => 'Sabre\\VObject\\Property\\Integer',
'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\\Integer',
'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\\Integer',
// 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',
);
/**
* Returns the current document type.
*
* @return void
*/
public function getDocumentType() {
return self::ICALENDAR20;
}
/**
* Returns a list of all 'base components'. For instance, if an Event has
@ -115,13 +250,19 @@ class VCalendar extends VObject\Document {
}
// Setting all properties to UTC time.
foreach($newEvents as $newEvent) {
foreach($newEvent->children as $child) {
if ($child instanceof VObject\Property\DateTime &&
$child->getDateType() == VObject\Property\DateTime::LOCALTZ) {
$child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC);
}
if ($child instanceof VObject\Property\ICalendar\DateTime && $child->hasTime()) {
$dt = $child->getDateTimes();
// 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);
@ -133,6 +274,21 @@ class VCalendar extends VObject\Document {
}
/**
* 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',
);
}
/**
* Validates the node for correctness.
* An array is returned with warnings.
@ -144,8 +300,7 @@ class VCalendar extends VObject\Document {
*
* @return array
*/
/*
public function validate() {
public function validate($options = 0) {
$warnings = array();
@ -188,39 +343,10 @@ class VCalendar extends VObject\Document {
);
}
$allowedComponents = array(
'VEVENT',
'VTODO',
'VJOURNAL',
'VFREEBUSY',
'VTIMEZONE',
);
$allowedProperties = array(
'PRODID',
'VERSION',
'CALSCALE',
'METHOD',
);
$componentsFound = 0;
foreach($this->children as $child) {
if($child instanceof Component) {
$componentsFound++;
if (!in_array($child->name, $allowedComponents)) {
$warnings[] = array(
'level' => 1,
'message' => 'The ' . $child->name . " component is not allowed in the VCALENDAR component",
'node' => $this,
);
}
}
if ($child instanceof Property) {
if (!in_array($child->name, $allowedProperties)) {
$warnings[] = array(
'level' => 2,
'message' => 'The ' . $child->name . " property is not allowed in the VCALENDAR component",
'node' => $this,
);
}
}
}
@ -238,7 +364,6 @@ class VCalendar extends VObject\Document {
);
}
*/
}

View file

@ -2,7 +2,8 @@
namespace Sabre\VObject\Component;
use Sabre\VObject;
use
Sabre\VObject;
/**
* The VCard component
@ -14,16 +15,164 @@ use Sabre\VObject;
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class VCard extends VObject\Component {
class VCard extends VObject\Document {
static $defaultName = 'VCARD';
/**
* The default name for this component.
*
* This should be 'VCALENDAR' or 'VCARD'.
*
* @var string
*/
static public $defaultName = 'VCARD';
/**
* Caching the version number
*
* @var int
*/
private $version = null;
/**
* List of value-types, and which classes they map to.
*
* @var array
*/
static public $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\\Float',
'INTEGER' => 'Sabre\\VObject\\Property\\Integer',
'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 public $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',
);
/**
* Returns the current document type.
*
* @return void
*/
public 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
*/
public 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, 4.0 is assumed.
* If the VCARD doesn't know its version, 2.1 is assumed.
*/
const DEFAULT_VERSION = '4.0';
const DEFAULT_VERSION = self::VCARD21;
/**
* Validates the node for correctness.
@ -46,6 +195,12 @@ class VCard extends VObject\Component {
$warnings = array();
$versionMap = array(
self::VCARD21 => '2.1',
self::VCARD30 => '3.0',
self::VCARD40 => '4.0',
);
$version = $this->select('VERSION');
if (count($version)!==1) {
$warnings[] = array(
@ -54,7 +209,7 @@ class VCard extends VObject\Component {
'node' => $this,
);
if ($options & self::REPAIR) {
$this->VERSION = self::DEFAULT_VERSION;
$this->VERSION = $versionMap[self::DEFAULT_VERSION];
}
} else {
$version = (string)$this->VERSION;
@ -65,7 +220,7 @@ class VCard extends VObject\Component {
'node' => $this,
);
if ($options & self::REPAIR) {
$this->VERSION = '4.0';
$this->VERSION = $versionMap[self::DEFAULT_VERSION];
}
}
@ -103,5 +258,96 @@ class VCard extends VObject\Component {
}
/**
* 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
*/
public 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
*/
public 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
*/
public 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

@ -55,7 +55,7 @@ class VEvent extends VObject\Component {
} elseif (isset($this->DURATION)) {
$effectiveEnd = clone $effectiveStart;
$effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) );
} elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
} elseif (!$this->DTSTART->hasTime()) {
$effectiveEnd = clone $effectiveStart;
$effectiveEnd->modify('+1 day');
} else {

View file

@ -31,7 +31,7 @@ class VJournal extends VObject\Component {
$dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
if ($dtstart) {
$effectiveEnd = clone $dtstart;
if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
if (!$this->DTSTART->hasTime()) {
$effectiveEnd->modify('+1 day');
}

View file

@ -28,7 +28,7 @@ class DateTimeParser {
static public function parseDateTime($dt,\DateTimeZone $tz = null) {
// Format is YYYYMMDD + "T" + hhmmss
$result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches);
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches);
if (!$result) {
throw new \LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt);
@ -40,7 +40,7 @@ class DateTimeParser {
$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'));
// $date->setTimeZone(new \DateTimeZone('UTC'));
return $date;
}
@ -54,7 +54,7 @@ class DateTimeParser {
static public function parseDate($date) {
// Format is YYYYMMDD
$result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
if (!$result) {
throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date);
@ -177,5 +177,239 @@ class DateTimeParser {
}
/**
* 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})?
(?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})?
(?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})?
(?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})?
(?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;
}
}

View file

@ -18,6 +18,36 @@ namespace Sabre\VObject;
*/
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.
*
@ -25,7 +55,28 @@ abstract class Document extends Component {
*
* @var string
*/
static $defaultName;
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.
@ -38,8 +89,8 @@ abstract class Document extends Component {
*
* So the two sigs:
*
* new Document(array $children = array());
* new Document(string $name, array $children = array())
* new Document(array $children = array(), $defaults = true);
* new Document(string $name, array $children = array(), $defaults = true)
*
* @return void
*/
@ -47,14 +98,50 @@ abstract class Document extends Component {
$args = func_get_args();
if (count($args)===0 || is_array($args[0])) {
array_unshift($args, static::$defaultName);
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
*
@ -65,23 +152,25 @@ abstract class Document extends Component {
* 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 = array()) {
public function createComponent($name, array $children = null, $defaults = true) {
$component = Component::create($name);
foreach($children as $k=>$v) {
if ($v instanceof Node) {
$component->add($v);
} else {
$component->add($k, $v);
}
$name = strtoupper($name);
$class = 'Sabre\\VObject\\Component';
if (isset(static::$componentMap[$name])) {
$class=static::$componentMap[$name];
}
return $component;
if (is_null($children)) $children = array();
return new $class($this, $name, $children, $defaults);
}
@ -98,11 +187,74 @@ abstract class Document extends Component {
* @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 = array()) {
public function createProperty($name, $value = null, array $parameters = null, $valueType = null) {
return Property::create($name, $value, $parameters);
// 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';
}
}

View file

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

View file

@ -2,6 +2,9 @@
namespace Sabre\VObject;
use
Sabre\VObject\Component\VCalendar;
/**
* This class helps with generating FREEBUSY reports based on existing sets of
* objects.
@ -204,7 +207,7 @@ class FreeBusyGenerator {
$duration = DateTimeParser::parseDuration((string)$component->DURATION);
$endTime = clone $startTime;
$endTime->add($duration);
} elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) {
} elseif (!$component->DTSTART->hasTime()) {
$endTime = clone $startTime;
$endTime->modify('+1 day');
} else {
@ -277,27 +280,24 @@ class FreeBusyGenerator {
if ($this->baseObject) {
$calendar = $this->baseObject;
} else {
$calendar = Component::create('VCALENDAR');
$calendar->version = '2.0';
$calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN';
$calendar->calscale = 'GREGORIAN';
$calendar = new VCalendar();
}
$vfreebusy = Component::create('VFREEBUSY');
$vfreebusy = $calendar->createComponent('VFREEBUSY');
$calendar->add($vfreebusy);
if ($this->start) {
$dtstart = Property::create('DTSTART');
$dtstart->setDateTime($this->start,Property\DateTime::UTC);
$dtstart = $calendar->createProperty('DTSTART');
$dtstart->setDateTime($this->start);
$vfreebusy->add($dtstart);
}
if ($this->end) {
$dtend = Property::create('DTEND');
$dtend->setDateTime($this->end,Property\DateTime::UTC);
$dtend = $calendar->createProperty('DTEND');
$dtend->setDateTime($this->end);
$vfreebusy->add($dtend);
}
$dtstamp = Property::create('DTSTAMP');
$dtstamp->setDateTime(new \DateTime('now'), Property\DateTime::UTC);
$dtstamp = $calendar->createProperty('DTSTAMP');
$dtstamp->setDateTime(new \DateTime('now', new \DateTimeZone('UTC')));
$vfreebusy->add($dtstamp);
foreach($busyTimes as $busyTime) {
@ -305,7 +305,7 @@ class FreeBusyGenerator {
$busyTime[0]->setTimeZone(new \DateTimeZone('UTC'));
$busyTime[1]->setTimeZone(new \DateTimeZone('UTC'));
$prop = Property::create(
$prop = $calendar->createProperty(
'FREEBUSY',
$busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z')
);

View file

@ -3,9 +3,9 @@
namespace Sabre\VObject;
/**
* Base class for all nodes
* A node is the root class for every element in an iCalendar of vCard object.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
@ -17,11 +17,11 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
const REPAIR = 1;
/**
* Turns the object back into a serialized blob.
* Reference to the parent object, if this is not the top object.
*
* @return string
* @var Node
*/
abstract function serialize();
public $parent;
/**
* Iterator override
@ -31,34 +31,26 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
protected $iterator = null;
/**
* A link to the parent node
* The root document
*
* @var Node
* @var Component
*/
public $parent = null;
protected $root;
/**
* Validates the node for correctness.
* Serializes the node into a mimedir format
*
* The following options are supported:
* - Node::REPAIR - If something is broken, and automatic repair may
* be attempted.
* @return string
*/
abstract 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.
*
* 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) {
return array();
}
abstract function jsonSerialize();
/* {{{ IteratorAggregator interface */
@ -90,6 +82,29 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
}
/**
* 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) {
return array();
}
/* }}} */
/* {{{ Countable interface */
@ -183,5 +198,4 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
// @codeCoverageIgnoreEnd
/* }}} */
}

View file

@ -2,6 +2,9 @@
namespace Sabre\VObject;
use
ArrayObject;
/**
* VObject Parameter
*
@ -23,41 +26,240 @@ class Parameter extends Node {
*/
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
*/
public $value;
protected $value;
/**
* Sets up the object
* Sets up the object.
*
* It's recommended to use the create:: factory method instead.
*
* @param string $name
* @param string $value
*/
public function __construct($name, $value = null) {
if (!is_scalar($value) && !is_null($value)) {
throw new \InvalidArgumentException('The value argument must be a scalar value or null');
}
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);
}
$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 2013 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 parameter's internal value.
* Returns the current value
*
* @return string
* 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() {
return $this->value;
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.
@ -66,27 +268,50 @@ class Parameter extends Node {
*/
public function serialize() {
if (is_null($this->value)) {
$value = $this->getParts();
if (count($value)===0) {
return $this->name;
}
$src = array(
'\\',
"\n",
';',
',',
);
$out = array(
'\\\\',
'\n',
'\;',
'\,',
);
$value = str_replace($src, $out, $this->value);
if (strpos($value,":")!==false) {
$value = '"' . $value . '"';
if ($this->root->getDocumentType() === Document::VCARD21 && $this->noName) {
return implode(';', $value);
}
return $this->name . '=' . $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
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;
}
@ -97,7 +322,21 @@ class Parameter extends Node {
*/
public function __toString() {
return $this->value;
return $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,188 @@
<?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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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,602 @@
<?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/vCard files and returns an array.
*
* The array is identical to the format jCard/jCal use.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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;
} else {
$this->input = $input;
}
}
/**
* Parses an entire document.
*
* @return void
*/
protected function parseDocument() {
$line = $this->readLine();
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 {
$rawLine = fgets($this->input);
if ($rawLine === false && feof($this->input)) {
throw new EofException('End of document reached prematurely');
}
$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['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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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);
}

View file

@ -3,152 +3,92 @@
namespace Sabre\VObject;
/**
* VObject Property
* Property
*
* A property in VObject is usually in the form PARAMNAME:paramValue.
* An example is : SUMMARY:Weekly meeting
* A property is always in a KEY:VALUE structure, and may optionally contain
* parameters.
*
* Properties can also have parameters:
* SUMMARY;LANG=en:Weekly meeting.
*
* Parameters can be accessed using the ArrayAccess interface.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Property extends Node {
abstract class Property extends Node {
/**
* Propertyname
* Property name.
*
* This will contain a string such as DTSTART, SUMMARY, FN.
*
* @var string
*/
public $name;
/**
* Group name
* Property group.
*
* This may be something like 'HOME' for vcards.
* This is only used in vcards
*
* @var string
*/
public $group;
/**
* Property parameters
* List of parameters
*
* @var array
*/
public $parameters = array();
/**
* Property value
* Current value
*
* @var string
* @var mixed
*/
public $value;
protected $value;
/**
* If properties are added to this map, they will be automatically mapped
* to their respective classes, if parsed by the reader or constructed with
* the 'create' method.
* In case this is a multi-value property. This string will be used as a
* delimiter.
*
* @var array
* @var string|null
*/
static public $classMap = array(
'COMPLETED' => 'Sabre\\VObject\\Property\\DateTime',
'CREATED' => 'Sabre\\VObject\\Property\\DateTime',
'DTEND' => 'Sabre\\VObject\\Property\\DateTime',
'DTSTAMP' => 'Sabre\\VObject\\Property\\DateTime',
'DTSTART' => 'Sabre\\VObject\\Property\\DateTime',
'DUE' => 'Sabre\\VObject\\Property\\DateTime',
'EXDATE' => 'Sabre\\VObject\\Property\\MultiDateTime',
'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\DateTime',
'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\DateTime',
'TRIGGER' => 'Sabre\\VObject\\Property\\DateTime',
'N' => 'Sabre\\VObject\\Property\\Compound',
'ORG' => 'Sabre\\VObject\\Property\\Compound',
'ADR' => 'Sabre\\VObject\\Property\\Compound',
'CATEGORIES' => 'Sabre\\VObject\\Property\\Compound',
);
public $delimiter = ';';
/**
* Creates the new property by name, but in addition will also see if
* there's a class mapped to the property name.
* Creates the generic property.
*
* Parameters can be specified with the optional third argument. Parameters
* must be a key->value map of the parameter name, and value. If the value
* is specified as an array, it is assumed that multiple parameters with
* the same name should be added.
* Parameters must be specified in key=>value syntax.
*
* @param Component $root The root document
* @param string $name
* @param string $value
* @param array $parameters
* @return Property
* @param string|array|null $value
* @param array $parameters List of parameters
* @param string $group The vcard property group
* @return void
*/
static public function create($name, $value = null, array $parameters = array()) {
public function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) {
$name = strtoupper($name);
$shortName = $name;
$group = null;
if (strpos($shortName,'.')!==false) {
list($group, $shortName) = explode('.', $shortName);
}
if (isset(self::$classMap[$shortName])) {
return new self::$classMap[$shortName]($name, $value, $parameters);
} else {
return new self($name, $value, $parameters);
}
}
/**
* Creates a new property object
*
* Parameters can be specified with the optional third argument. Parameters
* must be a key->value map of the parameter name, and value. If the value
* is specified as an array, it is assumed that multiple parameters with
* the same name should be added.
*
* @param string $name
* @param string $value
* @param array $parameters
*/
public function __construct($name, $value = null, array $parameters = array()) {
if (!is_scalar($value) && !is_null($value)) {
throw new \InvalidArgumentException('The value argument must be scalar or null');
}
$name = strtoupper($name);
$group = null;
if (strpos($name,'.')!==false) {
list($group, $name) = explode('.', $name);
}
$this->name = $name;
$this->group = $group;
$this->setValue($value);
foreach($parameters as $paramName => $paramValues) {
$this->root = $root;
if (!is_array($paramValues)) {
$paramValues = array($paramValues);
}
foreach($paramValues as $paramValue) {
$this->add($paramName, $paramValue);
}
if (!is_null($value)) {
$this->setValue($value);
}
foreach($parameters as $k=>$v) {
$this->add($k, $v);
}
}
/**
* Updates the internal value
* Updates the current value.
*
* @param string $value
* This may be either a single, or multiple strings in an array.
*
* @param string|array $value
* @return void
*/
public function setValue($value) {
@ -158,17 +98,131 @@ class Property extends Node {
}
/**
* Returns the internal 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.
*
* @param string $value
* @return string
*/
public function getValue() {
return $this->value;
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
*/
public 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
*/
public 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
*/
public 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
*/
public 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 public 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 public function setRawMimeDirValue($val);
/**
* Returns a raw mime-dir representation of the value.
*
* @return string
*/
abstract public function getRawMimeDirValue();
/**
* Turns the object back into a serialized blob.
*
@ -185,17 +239,7 @@ class Property extends Node {
}
$src = array(
'\\',
"\n",
"\r",
);
$out = array(
'\\\\',
'\n',
'',
);
$str.=':' . str_replace($src, $out, $this->value);
$str.=':' . $this->getRawMimeDirValue();
$out = '';
while(strlen($str)>0) {
@ -214,42 +258,84 @@ class Property extends Node {
}
/**
* Adds a new componenten or element
* Returns the value, in the format it should be encoded for json.
*
* You can call this method with the following syntaxes:
* This method must always return an array.
*
* add(Parameter $element)
* add(string $name, $value)
* @return array
*/
public function getJsonValue() {
return $this->getParts();
}
/**
* Sets the json value, as it would appear in a jCard or jCal object.
*
* The first version adds an Parameter
* The second adds a property as a string.
* The value must always be an array.
*
* @param mixed $item
* @param mixed $itemValue
* @param array $value
* @return void
*/
public function add($item, $itemValue = null) {
if ($item instanceof Parameter) {
if (!is_null($itemValue)) {
throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject');
}
$item->parent = $this;
$this->parameters[] = $item;
} elseif(is_string($item)) {
$parameter = new Parameter($item,$itemValue);
$parameter->parent = $this;
$this->parameters[] = $parameter;
public function setJsonValue(array $value) {
if (count($value)===1) {
$this->setValue(reset($value));
} else {
throw new \InvalidArgumentException('The first argument must either be a Node a string');
$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
*/
public 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
*/
public function __toString() {
return (string)$this->getValue();
}
/* ArrayAccess interface {{{ */
/**
@ -272,7 +358,9 @@ class Property extends Node {
}
/**
* Returns a parameter, or parameter list.
* Returns a parameter.
*
* If the parameter does not exist, null is returned.
*
* @param string $name
* @return Node
@ -282,20 +370,11 @@ class Property extends Node {
if (is_int($name)) return parent::offsetGet($name);
$name = strtoupper($name);
$result = array();
foreach($this->parameters as $parameter) {
if ($parameter->name == $name)
$result[] = $parameter;
if (!isset($this->parameters[$name])) {
return null;
}
if (count($result)===0) {
return null;
} elseif (count($result)===1) {
return $result[0];
} else {
$result[0]->setIterator(new ElementList($result));
return $result[0];
}
return $this->parameters[$name];
}
@ -308,27 +387,18 @@ class Property extends Node {
*/
public function offsetSet($name, $value) {
if (is_int($name)) parent::offsetSet($name, $value);
if (is_scalar($value)) {
if (!is_string($name))
throw new \InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.');
$this->offsetUnset($name);
$parameter = new Parameter($name, $value);
$parameter->parent = $this;
$this->parameters[] = $parameter;
} elseif ($value instanceof Parameter) {
if (!is_null($name))
throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\Sabre\\VObject\\Parameter. Add using $array[]=$parameterObject.');
$value->parent = $this;
$this->parameters[] = $value;
} else {
throw new \InvalidArgumentException('You can only add parameters to the property object');
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;
}
/**
@ -339,32 +409,20 @@ class Property extends Node {
*/
public function offsetUnset($name) {
if (is_int($name)) parent::offsetUnset($name);
$name = strtoupper($name);
foreach($this->parameters as $key=>$parameter) {
if ($parameter->name == $name) {
$parameter->parent = null;
unset($this->parameters[$key]);
}
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)]);
}
/* }}} */
/**
* Called when this object is being cast to a string
*
* @return string
*/
public function __toString() {
return (string)$this->value;
}
/**
* This method is automatically called when the object is cloned.
* Specifically, this will ensure all child elements are also cloned.
@ -402,14 +460,14 @@ class Property extends Node {
$warnings = array();
// Checking if our value is UTF-8
if (!StringUtil::isUTF8($this->value)) {
if (!StringUtil::isUTF8($this->getRawMimeDirValue())) {
$warnings[] = array(
'level' => 1,
'message' => 'Property is not valid UTF-8!',
'node' => $this,
);
if ($options & self::REPAIR) {
$this->value = StringUtil::convertToUTF8($this->value);
$this->setRawMimeDirValue(StringUtil::convertToUTF8($this->getRawMimeDirValue()));
}
}

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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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

@ -1,125 +0,0 @@
<?php
namespace Sabre\VObject\Property;
use Sabre\VObject;
/**
* Compound property.
*
* This class adds (de)serialization of compound properties to/from arrays.
*
* Currently the following properties from RFC 6350 are mapped to use this
* class:
*
* N: Section 6.2.2
* ADR: Section 6.3.1
* ORG: Section 6.6.4
* CATEGORIES: Section 6.7.1
*
* In order to use this correctly, you must call setParts and getParts to
* retrieve and modify dates respectively.
*
* @author Thomas Tanghus (http://tanghus.net/)
* @author Lars Kneschke
* @author Evert Pot (http://evertpot.com/)
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Compound extends VObject\Property {
/**
* If property names are added to this map, they will be (de)serialised as arrays
* using the getParts() and setParts() methods.
* The keys are the property names, values are delimiter chars.
*
* @var array
*/
static public $delimiterMap = array(
'N' => ';',
'ADR' => ';',
'ORG' => ';',
'CATEGORIES' => ',',
);
/**
* The currently used delimiter.
*
* @var string
*/
protected $delimiter = null;
/**
* Get a compound value as an array.
*
* @param $name string
* @return array
*/
public function getParts() {
if (is_null($this->value)) {
return array();
}
$delimiter = $this->getDelimiter();
// split by any $delimiter which is NOT prefixed by a slash.
// Note that this is not a a perfect solution. If a value is prefixed
// by two slashes, it should actually be split anyway.
//
// Hopefully we can fix this better in a future version, where we can
// break compatibility a bit.
$compoundValues = preg_split("/(?<!\\\)$delimiter/", $this->value);
// remove slashes from any semicolon and comma left escaped in the single values
$compoundValues = array_map(
function($val) {
return strtr($val, array('\,' => ',', '\;' => ';'));
}, $compoundValues);
return $compoundValues;
}
/**
* Returns the delimiter for this property.
*
* @return string
*/
public function getDelimiter() {
if (!$this->delimiter) {
if (isset(self::$delimiterMap[$this->name])) {
$this->delimiter = self::$delimiterMap[$this->name];
} else {
// To be a bit future proof, we are going to default the
// delimiter to ;
$this->delimiter = ';';
}
}
return $this->delimiter;
}
/**
* Set a compound value as an array.
*
*
* @param $name string
* @return array
*/
public function setParts(array $values) {
// add slashes to all semicolons and commas in the single values
$values = array_map(
function($val) {
return strtr($val, array(',' => '\,', ';' => '\;'));
}, $values);
$this->setValue(
implode($this->getDelimiter(), $values)
);
}
}

View file

@ -1,245 +0,0 @@
<?php
namespace Sabre\VObject\Property;
use Sabre\VObject;
/**
* DateTime property
*
* This element is used for iCalendar properties such as the DTSTART property.
* It basically provides a few helper functions that make it easier to deal
* with these. It supports both DATE-TIME and DATE values.
*
* In order to use this correctly, you must call setDateTime and getDateTime to
* retrieve and modify dates respectively.
*
* If you use the 'value' or properties directly, this object does not keep
* reference and results might appear incorrectly.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class DateTime extends VObject\Property {
/**
* Local 'floating' time
*/
const LOCAL = 1;
/**
* UTC-based time
*/
const UTC = 2;
/**
* Local time plus timezone
*/
const LOCALTZ = 3;
/**
* Only a date, time is ignored
*/
const DATE = 4;
/**
* DateTime representation
*
* @var \DateTime
*/
protected $dateTime;
/**
* dateType
*
* @var int
*/
protected $dateType;
/**
* Updates the Date and Time.
*
* @param \DateTime $dt
* @param int $dateType
* @return void
*/
public function setDateTime(\DateTime $dt, $dateType = self::LOCALTZ) {
switch($dateType) {
case self::LOCAL :
$this->setValue($dt->format('Ymd\\THis'));
$this->offsetUnset('VALUE');
$this->offsetUnset('TZID');
$this->offsetSet('VALUE','DATE-TIME');
break;
case self::UTC :
$dt->setTimeZone(new \DateTimeZone('UTC'));
$this->setValue($dt->format('Ymd\\THis\\Z'));
$this->offsetUnset('VALUE');
$this->offsetUnset('TZID');
$this->offsetSet('VALUE','DATE-TIME');
break;
case self::LOCALTZ :
$this->setValue($dt->format('Ymd\\THis'));
$this->offsetUnset('VALUE');
$this->offsetUnset('TZID');
$this->offsetSet('VALUE','DATE-TIME');
$this->offsetSet('TZID', $dt->getTimeZone()->getName());
break;
case self::DATE :
$this->setValue($dt->format('Ymd'));
$this->offsetUnset('VALUE');
$this->offsetUnset('TZID');
$this->offsetSet('VALUE','DATE');
break;
default :
throw new \InvalidArgumentException('You must pass a valid dateType constant');
}
$this->dateTime = $dt;
$this->dateType = $dateType;
}
/**
* Returns the current DateTime value.
*
* If no value was set, this method returns null.
*
* @return \DateTime|null
*/
public function getDateTime() {
if ($this->dateTime)
return $this->dateTime;
list(
$this->dateType,
$this->dateTime
) = self::parseData($this->value, $this);
return $this->dateTime;
}
/**
* Returns the type of Date format.
*
* This method returns one of the format constants. If no date was set,
* this method will return null.
*
* @return int|null
*/
public function getDateType() {
if ($this->dateType)
return $this->dateType;
list(
$this->dateType,
$this->dateTime,
) = self::parseData($this->value, $this);
return $this->dateType;
}
/**
* This method will return true, if the property had a date and a time, as
* opposed to only a date.
*
* @return bool
*/
public function hasTime() {
return $this->getDateType()!==self::DATE;
}
/**
* Parses the internal data structure to figure out what the current date
* and time is.
*
* The returned array contains two elements:
* 1. A 'DateType' constant (as defined on this class), or null.
* 2. A DateTime object (or null)
*
* @param string|null $propertyValue The string to parse (yymmdd or
* ymmddThhmmss, etc..)
* @param \Sabre\VObject\Property|null $property The instance of the
* property we're parsing.
* @return array
*/
static public function parseData($propertyValue, VObject\Property $property = null) {
if (is_null($propertyValue)) {
return array(null, null);
}
$date = '(?P<year>[1-2][0-9]{3})(?P<month>[0-1][0-9])(?P<date>[0-3][0-9])';
$time = '(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9])';
$regex = "/^$date(T$time(?P<isutc>Z)?)?$/";
if (!preg_match($regex, $propertyValue, $matches)) {
throw new \InvalidArgumentException($propertyValue . ' is not a valid \DateTime or Date string');
}
if (!isset($matches['hour'])) {
// Date-only
return array(
self::DATE,
new \DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00', new \DateTimeZone('UTC')),
);
}
$dateStr =
$matches['year'] .'-' .
$matches['month'] . '-' .
$matches['date'] . ' ' .
$matches['hour'] . ':' .
$matches['minute'] . ':' .
$matches['second'];
if (isset($matches['isutc'])) {
$dt = new \DateTime($dateStr,new \DateTimeZone('UTC'));
$dt->setTimeZone(new \DateTimeZone('UTC'));
return array(
self::UTC,
$dt
);
}
// Finding the timezone.
$tzid = $property['TZID'];
if (!$tzid) {
// This was a floating time string. This implies we use the
// timezone from date_default_timezone_set / date.timezone ini
// setting.
return array(
self::LOCAL,
new \DateTime($dateStr)
);
}
// To look up the timezone, we must first find the VCALENDAR component.
$root = $property;
while($root->parent) {
$root = $root->parent;
}
if ($root->name === 'VCALENDAR') {
$tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid, $root);
} else {
$tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid);
}
$dt = new \DateTime($dateStr, $tz);
$dt->setTimeZone($tz);
return array(
self::LOCALTZ,
$dt
);
}
}

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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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,101 @@
<?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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Float 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,41 @@
<?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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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';
}
}

View file

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

View file

@ -0,0 +1,308 @@
<?php
namespace Sabre\VObject\Property\ICalendar;
use
Sabre\VObject\Property,
Sabre\VObject\Parser\MimeDir,
Sabre\VObject\DateTimeParser,
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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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 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.
*
* @return \DateTime
*/
public function getDateTime() {
$dt = $this->getDateTimes();
if (!$dt) return null;
return $dt[0];
}
/**
* Returns multiple date-time values.
*
* @return \DateTime[]
*/
public function getDateTimes() {
// Finding the timezone.
$tz = $this['TZID'];
if ($tz) {
$tz = TimeZoneUtil::getTimeZone((string)$tz, $this->root);
}
$dts = array();
foreach($this->getParts() as $part) {
$dts[] = DateTimeParser::parse($part, $tz);
}
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();
$tz = $dts[0]->getTimeZone();
$isUtc = 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());
}
}

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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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,126 @@
<?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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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,189 @@
<?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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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)) {
$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;
}
$this->value = $newValue;
} 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);
}
}

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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Integer 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

@ -1,180 +0,0 @@
<?php
namespace Sabre\VObject\Property;
use Sabre\VObject;
/**
* Multi-DateTime property
*
* This element is used for iCalendar properties such as the EXDATE property.
* It basically provides a few helper functions that make it easier to deal
* with these. It supports both DATE-TIME and DATE values.
*
* In order to use this correctly, you must call setDateTimes and getDateTimes
* to retrieve and modify dates respectively.
*
* If you use the 'value' or properties directly, this object does not keep
* reference and results might appear incorrectly.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class MultiDateTime extends VObject\Property {
/**
* DateTime representation
*
* @var DateTime[]
*/
protected $dateTimes;
/**
* dateType
*
* This is one of the Sabre\VObject\Property\DateTime constants.
*
* @var int
*/
protected $dateType;
/**
* Updates the value
*
* @param array $dt Must be an array of DateTime objects.
* @param int $dateType
* @return void
*/
public function setDateTimes(array $dt, $dateType = VObject\Property\DateTime::LOCALTZ) {
foreach($dt as $i)
if (!$i instanceof \DateTime)
throw new \InvalidArgumentException('You must pass an array of DateTime objects');
$this->offsetUnset('VALUE');
$this->offsetUnset('TZID');
switch($dateType) {
case DateTime::LOCAL :
$val = array();
foreach($dt as $i) {
$val[] = $i->format('Ymd\\THis');
}
$this->setValue(implode(',',$val));
$this->offsetSet('VALUE','DATE-TIME');
break;
case DateTime::UTC :
$val = array();
foreach($dt as $i) {
$i->setTimeZone(new \DateTimeZone('UTC'));
$val[] = $i->format('Ymd\\THis\\Z');
}
$this->setValue(implode(',',$val));
$this->offsetSet('VALUE','DATE-TIME');
break;
case DateTime::LOCALTZ :
$val = array();
foreach($dt as $i) {
$val[] = $i->format('Ymd\\THis');
}
$this->setValue(implode(',',$val));
$this->offsetSet('VALUE','DATE-TIME');
$this->offsetSet('TZID', $dt[0]->getTimeZone()->getName());
break;
case DateTime::DATE :
$val = array();
foreach($dt as $i) {
$val[] = $i->format('Ymd');
}
$this->setValue(implode(',',$val));
$this->offsetSet('VALUE','DATE');
break;
default :
throw new \InvalidArgumentException('You must pass a valid dateType constant');
}
$this->dateTimes = $dt;
$this->dateType = $dateType;
}
/**
* Returns the current DateTime value.
*
* If no value was set, this method returns null.
*
* @return array|null
*/
public function getDateTimes() {
if ($this->dateTimes)
return $this->dateTimes;
$dts = array();
if (!$this->value) {
$this->dateTimes = null;
$this->dateType = null;
return null;
}
foreach(explode(',',$this->value) as $val) {
list(
$type,
$dt
) = DateTime::parseData($val, $this);
$dts[] = $dt;
$this->dateType = $type;
}
$this->dateTimes = $dts;
return $this->dateTimes;
}
/**
* Returns the type of Date format.
*
* This method returns one of the format constants. If no date was set,
* this method will return null.
*
* @return int|null
*/
public function getDateType() {
if ($this->dateType)
return $this->dateType;
if (!$this->value) {
$this->dateTimes = null;
$this->dateType = null;
return null;
}
$dts = array();
foreach(explode(',',$this->value) as $val) {
list(
$type,
$dt
) = DateTime::parseData($val, $this);
$dts[] = $dt;
$this->dateType = $type;
}
$this->dateTimes = $dts;
return $this->dateType;
}
/**
* This method will return true, if the property had a date and a time, as
* opposed to only a date.
*
* @return bool
*/
public function hasTime() {
return $this->getDateType()!==DateTime::DATE;
}
}

View file

@ -0,0 +1,330 @@
<?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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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,70 @@
<?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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Uri extends Property {
/**
* 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) {
$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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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,33 @@
<?php
namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser;
/**
* Date property
*
* This object encodes vCard DATE values.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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";
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace Sabre\VObject\Property\VCard;
use
Sabre\VObject\DateTimeParser,
Sabre\VObject\Property\Text;
/**
* DateAndOrTime property
*
* This object encodes DATE-AND-OR-TIME values.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class DateAndOrTime extends Text {
/**
* 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";
}
/**
* 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);
}
}

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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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->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 "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) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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);
}
}

View file

@ -3,12 +3,10 @@
namespace Sabre\VObject;
/**
* VCALENDAR/VCARD reader
* iCalendar/vCard/jCal/jCard reader object.
*
* This class reads the vobject file, and returns a full element tree.
*
* TODO: this class currently completely works 'statically'. This is pointless,
* and defeats OOP principals. Needs refactoring in a future version.
* This object provides a few (static) convenience methods to quickly access
* the parsers.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
@ -19,9 +17,6 @@ class Reader {
/**
* If this option is passed to the reader, it will be less strict about the
* validity of the lines.
*
* Currently using this option just means, that it will accept underscores
* in property names.
*/
const OPTION_FORGIVING = 1;
@ -32,192 +27,47 @@ class Reader {
const OPTION_IGNORE_INVALID_LINES = 2;
/**
* Parses the file and returns the top component
* 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.
*
* @param string $data
* You can either supply a string, or a readable stream for input.
*
* @param string|resource $data
* @param int $options
* @return Node
* @return Document
*/
static function read($data, $options = 0) {
// Normalizing newlines
$data = str_replace(array("\r","\n\n"), array("\n","\n"), $data);
$parser = new Parser\MimeDir();
$result = $parser->parse($data, $options);
$lines = explode("\n", $data);
// Unfolding lines
$lines2 = array();
foreach($lines as $line) {
// Skipping empty lines
if (!$line) continue;
if ($line[0]===" " || $line[0]==="\t") {
$lines2[count($lines2)-1].=substr($line,1);
} else {
$lines2[] = $line;
}
}
unset($lines);
reset($lines2);
return self::readLine($lines2, $options);
return $result;
}
/**
* Reads and parses a single line.
* Parses a jCard or jCal object, and returns the top component.
*
* This method receives the full array of lines. The array pointer is used
* to traverse.
* The options argument is a bitfield. Pass any of the OPTIONS constant to
* alter the parsers' behaviour.
*
* This method returns null if an invalid line was encountered, and the
* IGNORE_INVALID_LINES option was turned on.
* 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 array $lines
* @param int $options See the OPTIONS constants.
* @param string|resource|array $data
* @param int $options
* @return Node
*/
static private function readLine(&$lines, $options = 0) {
static function readJson($data, $options = 0) {
$line = current($lines);
$lineNr = key($lines);
next($lines);
// Components
if (strtoupper(substr($line,0,6)) === "BEGIN:") {
$componentName = strtoupper(substr($line,6));
$obj = Component::create($componentName);
$nextLine = current($lines);
while(strtoupper(substr($nextLine,0,4))!=="END:") {
$parsedLine = self::readLine($lines, $options);
$nextLine = current($lines);
if (is_null($parsedLine)) {
continue;
}
$obj->add($parsedLine);
if ($nextLine===false)
throw new ParseException('Invalid VObject. Document ended prematurely.');
}
// Checking component name of the 'END:' line.
if (substr($nextLine,4)!==$obj->name) {
throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"');
}
next($lines);
return $obj;
}
// Properties
//$result = preg_match('/(?P<name>[A-Z0-9-]+)(?:;(?P<parameters>^(?<!:):))(.*)$/',$line,$matches);
if ($options & self::OPTION_FORGIVING) {
$token = '[A-Z0-9-\._]+';
} else {
$token = '[A-Z0-9-\.]+';
}
$parameters = "(?:;(?P<parameters>([^:^\"]|\"([^\"]*)\")*))?";
$regex = "/^(?P<name>$token)$parameters:(?P<value>.*)$/i";
$result = preg_match($regex,$line,$matches);
if (!$result) {
if ($options & self::OPTION_IGNORE_INVALID_LINES) {
return null;
} else {
throw new ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format');
}
}
$propertyName = strtoupper($matches['name']);
$propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n))#',function($matches) {
if ($matches[2]==='n' || $matches[2]==='N') {
return "\n";
} else {
return $matches[2];
}
}, $matches['value']);
$obj = Property::create($propertyName, $propertyValue);
if ($matches['parameters']) {
foreach(self::readParameters($matches['parameters']) as $param) {
$obj->add($param);
}
}
return $obj;
$parser = new Parser\Json();
$result = $parser->parse($data, $options);
return $result;
}
/**
* Reads a parameter list from a property
*
* This method returns an array of Parameter
*
* @param string $parameters
* @return array
*/
static private function readParameters($parameters) {
$token = '[A-Z0-9-]+';
$paramValue = '(?P<paramValue>[^\"^;]*|"[^"]*")';
$regex = "/(?<=^|;)(?P<paramName>$token)(=$paramValue(?=$|;))?/i";
preg_match_all($regex, $parameters, $matches, PREG_SET_ORDER);
$params = array();
foreach($matches as $match) {
if (!isset($match['paramValue'])) {
$value = null;
} else {
$value = $match['paramValue'];
if (isset($value[0]) && $value[0]==='"') {
// Stripping quotes, if needed
$value = substr($value,1,strlen($value)-2);
}
$value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) {
if ($matches[2]==='n' || $matches[2]==='N') {
return "\n";
} else {
return $matches[2];
}
}, $value);
}
$params[] = new Parameter($match['paramName'], $value);
}
return $params;
}
}

View file

@ -41,6 +41,8 @@ namespace Sabre\VObject;
* 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) 2007-2013 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
@ -70,7 +72,6 @@ class RecurrenceIterator implements \Iterator {
*/
public $currentDate;
/**
* List of dates that are excluded from the rules.
*
@ -314,7 +315,7 @@ class RecurrenceIterator implements \Iterator {
public function __construct(Component $vcal, $uid=null) {
if (is_null($uid)) {
if ($vcal->name === 'VCALENDAR') {
if ($vcal instanceof Component\VCalendar) {
throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well');
}
$components = array($vcal);
@ -336,7 +337,17 @@ class RecurrenceIterator implements \Iterator {
ksort($this->overriddenEvents);
if (!$this->baseEvent) {
throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid);
// No base event was found. CalDAV does allow cases where only
// overridden instances are stored.
//
// In this barticular 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('Could not find an event with uid: ' . $uid);
}
ksort($this->overriddenEvents, SORT_NUMERIC);
$this->baseEvent = array_shift($this->overriddenEvents);
}
$this->startDate = clone $this->baseEvent->DTSTART->getDateTime();
@ -347,26 +358,22 @@ class RecurrenceIterator implements \Iterator {
} else {
$this->endDate = clone $this->startDate;
if (isset($this->baseEvent->DURATION)) {
$this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value));
} elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) {
$this->endDate->add(DateTimeParser::parse((string)$this->baseEvent->DURATION));
} elseif (!$this->baseEvent->DTSTART->hasTime()) {
$this->endDate->modify('+1 day');
}
}
$this->currentDate = clone $this->startDate;
$rrule = (string)$this->baseEvent->RRULE;
$parts = explode(';', $rrule);
$rrule = $this->baseEvent->RRULE;
// If no rrule was specified, we create a default setting
if (!$rrule) {
$this->frequency = 'daily';
$this->count = 1;
} else foreach($parts as $part) {
} else foreach($rrule->getParts() as $key=>$value) {
list($key, $value) = explode('=', $part, 2);
switch(strtoupper($key)) {
switch($key) {
case 'FREQ' :
if (!in_array(
@ -407,39 +414,39 @@ class RecurrenceIterator implements \Iterator {
break;
case 'BYSECOND' :
$this->bySecond = explode(',', $value);
$this->bySecond = (array)$value;
break;
case 'BYMINUTE' :
$this->byMinute = explode(',', $value);
$this->byMinute = (array)$value;
break;
case 'BYHOUR' :
$this->byHour = explode(',', $value);
$this->byHour = (array)$value;
break;
case 'BYDAY' :
$this->byDay = explode(',', strtoupper($value));
$this->byDay = (array)$value;
break;
case 'BYMONTHDAY' :
$this->byMonthDay = explode(',', $value);
$this->byMonthDay = (array)$value;
break;
case 'BYYEARDAY' :
$this->byYearDay = explode(',', $value);
$this->byYearDay = (array)$value;
break;
case 'BYWEEKNO' :
$this->byWeekNo = explode(',', $value);
$this->byWeekNo = (array)$value;
break;
case 'BYMONTH' :
$this->byMonth = explode(',', $value);
$this->byMonth = (array)$value;
break;
case 'BYSETPOS' :
$this->bySetPos = explode(',', $value);
$this->bySetPos = (array)$value;
break;
case 'WKST' :
@ -528,9 +535,9 @@ class RecurrenceIterator implements \Iterator {
unset($event->RDATE);
unset($event->EXRULE);
$event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType());
$event->DTSTART->setDateTime($this->getDTStart());
if (isset($event->DTEND)) {
$event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
$event->DTEND->setDateTime($this->getDtEnd());
}
if ($this->counter > 0) {
$event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
@ -740,7 +747,9 @@ class RecurrenceIterator implements \Iterator {
$this->currentDate->modify('+' . $this->interval . ' hours');
return;
}
// @codeCoverageIgnoreStart
}
// @codeCoverageIgnoreEnd
/**
* Does the processing for advancing the iterator for daily frequency.

View file

@ -2,7 +2,9 @@
namespace Sabre\VObject\Splitter;
use Sabre\VObject;
use
Sabre\VObject,
Sabre\VObject\Component\VCalendar;
/**
* Splitter
@ -40,14 +42,15 @@ class ICalendar implements SplitterInterface {
* 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) {
public function __construct($input, $options = 0) {
$data = VObject\Reader::read(stream_get_contents($input));
$data = VObject\Reader::read($input, $options);
$vtimezones = array();
$components = array();
foreach($data->children as $component) {
foreach($data->children() as $component) {
if (!$component instanceof VObject\Component) {
continue;
}
@ -68,7 +71,7 @@ class ICalendar implements SplitterInterface {
// Take care of recurring events
if (!array_key_exists($uid, $this->objects)) {
$this->objects[$uid] = VObject\Component::create('VCALENDAR');
$this->objects[$uid] = new VCalendar();
}
$this->objects[$uid]->add(clone $component);

View file

@ -2,7 +2,9 @@
namespace Sabre\VObject\Splitter;
use Sabre\VObject;
use
Sabre\VObject,
Sabre\VObject\Parser\MimeDir;
/**
* Splitter
@ -27,16 +29,25 @@ class VCard implements SplitterInterface {
*/
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) {
public function __construct($input, $options = 0) {
$this->input = $input;
$this->parser = new MimeDir($input, $options);
}
@ -50,23 +61,10 @@ class VCard implements SplitterInterface {
*/
public function getNext() {
$vcard = '';
do {
if (feof($this->input)) {
return false;
}
$line = fgets($this->input);
$vcard .= $line;
} while(strtoupper(substr($line,0,4))!=="END:");
$object = VObject\Reader::read($vcard);
if($object->name !== 'VCARD') {
throw new \InvalidArgumentException("Thats no vCard!", 1);
try {
$object = $this->parser->parse();
} catch (VObject\EofException $e) {
return null;
}
return $object;

View file

@ -452,7 +452,7 @@ class TimeZoneUtil {
// 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'}->value;
$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) {

View file

@ -0,0 +1,382 @@
<?php
namespace Sabre\VObject;
/**
* This utility converts vcards from one version to another.
*
* @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/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 = null;
if ($targetVersion===Document::VCARD30) {
if ($property instanceof Property\Uri && in_array($property->name, array('PHOTO','LOGO','SOUND'))) {
$newProperty = $this->convertUriToBinary($output, $property, $parameters);
} elseif ($property->name === 'KIND') {
switch(strtolower($property->getValue())) {
case 'org' :
// OS X addressbook property.
$newProperty = $output->createProperty('X-ABSHOWAS','COMPANY');
break;
case 'individual' :
// Individual is implied, so we can just 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, $property, $parameters);
} else {
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;
}
}
}
if (is_null($newProperty)) {
$newProperty = $output->createProperty(
$property->name,
$property->getParts(),
array(), // no parameters yet
$valueType
);
}
// 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 $property, array &$parameters) {
$newProperty = $output->createProperty(
$property->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($property->getValue()));
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.
* @param $parameters List of parameters that will eventually be added to
* the new property.
* @return Property\Binary|null
*/
protected function convertUriToBinary(Component\VCard $output, Property\Uri $property, array &$parameters) {
$value = $property->getValue();
// Only converting data: uris
if (substr($value, 0, 5)!=='data:') {
return;
}
$newProperty = $output->createProperty(
$property->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;
}
}
}
}

View file

@ -14,11 +14,6 @@ class Version {
/**
* Full version number
*/
const VERSION = '2.1.3';
/**
* Stability : alpha, beta, stable
*/
const STABILITY = 'stable';
const VERSION = '3.1.3';
}

View file

@ -12,30 +12,55 @@
*/
// Begin includes
include __DIR__ . '/Cli.php';
include __DIR__ . '/DateTimeParser.php';
include __DIR__ . '/ElementList.php';
include __DIR__ . '/FreeBusyGenerator.php';
include __DIR__ . '/Node.php';
include __DIR__ . '/Parameter.php';
include __DIR__ . '/ParseException.php';
include __DIR__ . '/Parser/Parser.php';
include __DIR__ . '/Property.php';
include __DIR__ . '/Reader.php';
include __DIR__ . '/RecurrenceIterator.php';
include __DIR__ . '/Splitter/SplitterInterface.php';
include __DIR__ . '/Splitter/VCard.php';
include __DIR__ . '/StringUtil.php';
include __DIR__ . '/TimeZoneUtil.php';
include __DIR__ . '/VCardConverter.php';
include __DIR__ . '/Version.php';
include __DIR__ . '/Splitter/VCard.php';
include __DIR__ . '/Component.php';
include __DIR__ . '/Document.php';
include __DIR__ . '/Property/Compound.php';
include __DIR__ . '/Property/DateTime.php';
include __DIR__ . '/Property/MultiDateTime.php';
include __DIR__ . '/EofException.php';
include __DIR__ . '/Parser/Json.php';
include __DIR__ . '/Parser/MimeDir.php';
include __DIR__ . '/Property/Binary.php';
include __DIR__ . '/Property/Boolean.php';
include __DIR__ . '/Property/Float.php';
include __DIR__ . '/Property/ICalendar/DateTime.php';
include __DIR__ . '/Property/ICalendar/Duration.php';
include __DIR__ . '/Property/ICalendar/Period.php';
include __DIR__ . '/Property/ICalendar/Recur.php';
include __DIR__ . '/Property/Integer.php';
include __DIR__ . '/Property/Text.php';
include __DIR__ . '/Property/Time.php';
include __DIR__ . '/Property/Unknown.php';
include __DIR__ . '/Property/Uri.php';
include __DIR__ . '/Property/UtcOffset.php';
include __DIR__ . '/Property/VCard/DateAndOrTime.php';
include __DIR__ . '/Property/VCard/DateTime.php';
include __DIR__ . '/Property/VCard/LanguageTag.php';
include __DIR__ . '/Property/VCard/TimeStamp.php';
include __DIR__ . '/Splitter/ICalendar.php';
include __DIR__ . '/Component/VAlarm.php';
include __DIR__ . '/Component/VCalendar.php';
include __DIR__ . '/Component/VCard.php';
include __DIR__ . '/Component/VEvent.php';
include __DIR__ . '/Component/VFreeBusy.php';
include __DIR__ . '/Component/VJournal.php';
include __DIR__ . '/Component/VTodo.php';
include __DIR__ . '/Property/FlatText.php';
include __DIR__ . '/Property/ICalendar/CalAddress.php';
include __DIR__ . '/Property/ICalendar/Date.php';
include __DIR__ . '/Property/VCard/Date.php';
// End includes