1<?php 2 3namespace Sabre\VObject\Parser; 4 5use Sabre\VObject\Component; 6use Sabre\VObject\Component\VCalendar; 7use Sabre\VObject\Component\VCard; 8use Sabre\VObject\EofException; 9use Sabre\VObject\ParseException; 10use Sabre\Xml as SabreXml; 11 12/** 13 * XML Parser. 14 * 15 * This parser parses both the xCal and xCard formats. 16 * 17 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 18 * @author Ivan Enderlin 19 * @license http://sabre.io/license/ Modified BSD License 20 */ 21class XML extends Parser 22{ 23 const XCAL_NAMESPACE = 'urn:ietf:params:xml:ns:icalendar-2.0'; 24 const XCARD_NAMESPACE = 'urn:ietf:params:xml:ns:vcard-4.0'; 25 26 /** 27 * The input data. 28 * 29 * @var array 30 */ 31 protected $input; 32 33 /** 34 * A pointer/reference to the input. 35 * 36 * @var array 37 */ 38 private $pointer; 39 40 /** 41 * Document, root component. 42 * 43 * @var \Sabre\VObject\Document 44 */ 45 protected $root; 46 47 /** 48 * Creates the parser. 49 * 50 * Optionally, it's possible to parse the input stream here. 51 * 52 * @param mixed $input 53 * @param int $options any parser options (OPTION constants) 54 */ 55 public function __construct($input = null, $options = 0) 56 { 57 if (0 === $options) { 58 $options = parent::OPTION_FORGIVING; 59 } 60 61 parent::__construct($input, $options); 62 } 63 64 /** 65 * Parse xCal or xCard. 66 * 67 * @param resource|string $input 68 * @param int $options 69 * 70 * @throws \Exception 71 * 72 * @return \Sabre\VObject\Document 73 */ 74 public function parse($input = null, $options = 0) 75 { 76 if (!is_null($input)) { 77 $this->setInput($input); 78 } 79 80 if (0 !== $options) { 81 $this->options = $options; 82 } 83 84 if (is_null($this->input)) { 85 throw new EofException('End of input stream, or no input supplied'); 86 } 87 88 switch ($this->input['name']) { 89 case '{'.self::XCAL_NAMESPACE.'}icalendar': 90 $this->root = new VCalendar([], false); 91 $this->pointer = &$this->input['value'][0]; 92 $this->parseVCalendarComponents($this->root); 93 break; 94 95 case '{'.self::XCARD_NAMESPACE.'}vcards': 96 foreach ($this->input['value'] as &$vCard) { 97 $this->root = new VCard(['version' => '4.0'], false); 98 $this->pointer = &$vCard; 99 $this->parseVCardComponents($this->root); 100 101 // We just parse the first <vcard /> element. 102 break; 103 } 104 break; 105 106 default: 107 throw new ParseException('Unsupported XML standard'); 108 } 109 110 return $this->root; 111 } 112 113 /** 114 * Parse a xCalendar component. 115 * 116 * @param Component $parentComponent 117 */ 118 protected function parseVCalendarComponents(Component $parentComponent) 119 { 120 foreach ($this->pointer['value'] ?: [] as $children) { 121 switch (static::getTagName($children['name'])) { 122 case 'properties': 123 $this->pointer = &$children['value']; 124 $this->parseProperties($parentComponent); 125 break; 126 127 case 'components': 128 $this->pointer = &$children; 129 $this->parseComponent($parentComponent); 130 break; 131 } 132 } 133 } 134 135 /** 136 * Parse a xCard component. 137 * 138 * @param Component $parentComponent 139 */ 140 protected function parseVCardComponents(Component $parentComponent) 141 { 142 $this->pointer = &$this->pointer['value']; 143 $this->parseProperties($parentComponent); 144 } 145 146 /** 147 * Parse xCalendar and xCard properties. 148 * 149 * @param Component $parentComponent 150 * @param string $propertyNamePrefix 151 */ 152 protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '') 153 { 154 foreach ($this->pointer ?: [] as $xmlProperty) { 155 list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); 156 157 $propertyName = $tagName; 158 $propertyValue = []; 159 $propertyParameters = []; 160 $propertyType = 'text'; 161 162 // A property which is not part of the standard. 163 if (self::XCAL_NAMESPACE !== $namespace 164 && self::XCARD_NAMESPACE !== $namespace) { 165 $propertyName = 'xml'; 166 $value = '<'.$tagName.' xmlns="'.$namespace.'"'; 167 168 foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) { 169 $value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"'; 170 } 171 172 $value .= '>'.$xmlProperty['value'].'</'.$tagName.'>'; 173 174 $propertyValue = [$value]; 175 176 $this->createProperty( 177 $parentComponent, 178 $propertyName, 179 $propertyParameters, 180 $propertyType, 181 $propertyValue 182 ); 183 184 continue; 185 } 186 187 // xCard group. 188 if ('group' === $propertyName) { 189 if (!isset($xmlProperty['attributes']['name'])) { 190 continue; 191 } 192 193 $this->pointer = &$xmlProperty['value']; 194 $this->parseProperties( 195 $parentComponent, 196 strtoupper($xmlProperty['attributes']['name']).'.' 197 ); 198 199 continue; 200 } 201 202 // Collect parameters. 203 foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) { 204 if (!is_array($xmlPropertyChild) 205 || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) { 206 continue; 207 } 208 209 $xmlParameters = $xmlPropertyChild['value']; 210 211 foreach ($xmlParameters as $xmlParameter) { 212 $propertyParameterValues = []; 213 214 foreach ($xmlParameter['value'] as $xmlParameterValues) { 215 $propertyParameterValues[] = $xmlParameterValues['value']; 216 } 217 218 $propertyParameters[static::getTagName($xmlParameter['name'])] 219 = implode(',', $propertyParameterValues); 220 } 221 222 array_splice($xmlProperty['value'], $i, 1); 223 } 224 225 $propertyNameExtended = ($this->root instanceof VCalendar 226 ? 'xcal' 227 : 'xcard').':'.$propertyName; 228 229 switch ($propertyNameExtended) { 230 case 'xcal:geo': 231 $propertyType = 'float'; 232 $propertyValue['latitude'] = 0; 233 $propertyValue['longitude'] = 0; 234 235 foreach ($xmlProperty['value'] as $xmlRequestChild) { 236 $propertyValue[static::getTagName($xmlRequestChild['name'])] 237 = $xmlRequestChild['value']; 238 } 239 break; 240 241 case 'xcal:request-status': 242 $propertyType = 'text'; 243 244 foreach ($xmlProperty['value'] as $xmlRequestChild) { 245 $propertyValue[static::getTagName($xmlRequestChild['name'])] 246 = $xmlRequestChild['value']; 247 } 248 break; 249 250 case 'xcal:freebusy': 251 $propertyType = 'freebusy'; 252 // We don't break because we only want to set 253 // another property type. 254 255 // no break 256 case 'xcal:categories': 257 case 'xcal:resources': 258 case 'xcal:exdate': 259 foreach ($xmlProperty['value'] as $specialChild) { 260 $propertyValue[static::getTagName($specialChild['name'])] 261 = $specialChild['value']; 262 } 263 break; 264 265 case 'xcal:rdate': 266 $propertyType = 'date-time'; 267 268 foreach ($xmlProperty['value'] as $specialChild) { 269 $tagName = static::getTagName($specialChild['name']); 270 271 if ('period' === $tagName) { 272 $propertyParameters['value'] = 'PERIOD'; 273 $propertyValue[] = implode('/', $specialChild['value']); 274 } else { 275 $propertyValue[] = $specialChild['value']; 276 } 277 } 278 break; 279 280 default: 281 $propertyType = static::getTagName($xmlProperty['value'][0]['name']); 282 283 foreach ($xmlProperty['value'] as $value) { 284 $propertyValue[] = $value['value']; 285 } 286 287 if ('date' === $propertyType) { 288 $propertyParameters['value'] = 'DATE'; 289 } 290 break; 291 } 292 293 $this->createProperty( 294 $parentComponent, 295 $propertyNamePrefix.$propertyName, 296 $propertyParameters, 297 $propertyType, 298 $propertyValue 299 ); 300 } 301 } 302 303 /** 304 * Parse a component. 305 * 306 * @param Component $parentComponent 307 */ 308 protected function parseComponent(Component $parentComponent) 309 { 310 $components = $this->pointer['value'] ?: []; 311 312 foreach ($components as $component) { 313 $componentName = static::getTagName($component['name']); 314 $currentComponent = $this->root->createComponent( 315 $componentName, 316 null, 317 false 318 ); 319 320 $this->pointer = &$component; 321 $this->parseVCalendarComponents($currentComponent); 322 323 $parentComponent->add($currentComponent); 324 } 325 } 326 327 /** 328 * Create a property. 329 * 330 * @param Component $parentComponent 331 * @param string $name 332 * @param array $parameters 333 * @param string $type 334 * @param mixed $value 335 */ 336 protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value) 337 { 338 $property = $this->root->createProperty( 339 $name, 340 null, 341 $parameters, 342 $type 343 ); 344 $parentComponent->add($property); 345 $property->setXmlValue($value); 346 } 347 348 /** 349 * Sets the input data. 350 * 351 * @param resource|string $input 352 */ 353 public function setInput($input) 354 { 355 if (is_resource($input)) { 356 $input = stream_get_contents($input); 357 } 358 359 if (is_string($input)) { 360 $reader = new SabreXml\Reader(); 361 $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period'] 362 = 'Sabre\VObject\Parser\XML\Element\KeyValue'; 363 $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur'] 364 = 'Sabre\VObject\Parser\XML\Element\KeyValue'; 365 $reader->xml($input); 366 $input = $reader->parse(); 367 } 368 369 $this->input = $input; 370 } 371 372 /** 373 * Get tag name from a Clark notation. 374 * 375 * @param string $clarkedTagName 376 * 377 * @return string 378 */ 379 protected static function getTagName($clarkedTagName) 380 { 381 list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName); 382 383 return $tagName; 384 } 385} 386