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 * @return void 56 */ 57 function __construct($input = null, $options = 0) { 58 59 if (0 === $options) { 60 $options = parent::OPTION_FORGIVING; 61 } 62 63 parent::__construct($input, $options); 64 65 } 66 67 /** 68 * Parse xCal or xCard. 69 * 70 * @param resource|string $input 71 * @param int $options 72 * 73 * @throws \Exception 74 * 75 * @return Sabre\VObject\Document 76 */ 77 function parse($input = null, $options = 0) { 78 79 if (!is_null($input)) { 80 $this->setInput($input); 81 } 82 83 if (0 !== $options) { 84 $this->options = $options; 85 } 86 87 if (is_null($this->input)) { 88 throw new EofException('End of input stream, or no input supplied'); 89 } 90 91 switch ($this->input['name']) { 92 93 case '{' . self::XCAL_NAMESPACE . '}icalendar': 94 $this->root = new VCalendar([], false); 95 $this->pointer = &$this->input['value'][0]; 96 $this->parseVCalendarComponents($this->root); 97 break; 98 99 case '{' . self::XCARD_NAMESPACE . '}vcards': 100 foreach ($this->input['value'] as &$vCard) { 101 102 $this->root = new VCard(['version' => '4.0'], false); 103 $this->pointer = &$vCard; 104 $this->parseVCardComponents($this->root); 105 106 // We just parse the first <vcard /> element. 107 break; 108 109 } 110 break; 111 112 default: 113 throw new ParseException('Unsupported XML standard'); 114 115 } 116 117 return $this->root; 118 } 119 120 /** 121 * Parse a xCalendar component. 122 * 123 * @param Component $parentComponent 124 * 125 * @return void 126 */ 127 protected function parseVCalendarComponents(Component $parentComponent) { 128 129 foreach ($this->pointer['value'] ?: [] as $children) { 130 131 switch (static::getTagName($children['name'])) { 132 133 case 'properties': 134 $this->pointer = &$children['value']; 135 $this->parseProperties($parentComponent); 136 break; 137 138 case 'components': 139 $this->pointer = &$children; 140 $this->parseComponent($parentComponent); 141 break; 142 } 143 } 144 145 } 146 147 /** 148 * Parse a xCard component. 149 * 150 * @param Component $parentComponent 151 * 152 * @return void 153 */ 154 protected function parseVCardComponents(Component $parentComponent) { 155 156 $this->pointer = &$this->pointer['value']; 157 $this->parseProperties($parentComponent); 158 159 } 160 161 /** 162 * Parse xCalendar and xCard properties. 163 * 164 * @param Component $parentComponent 165 * @param string $propertyNamePrefix 166 * 167 * @return void 168 */ 169 protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '') { 170 171 foreach ($this->pointer ?: [] as $xmlProperty) { 172 173 list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); 174 175 $propertyName = $tagName; 176 $propertyValue = []; 177 $propertyParameters = []; 178 $propertyType = 'text'; 179 180 // A property which is not part of the standard. 181 if ($namespace !== self::XCAL_NAMESPACE 182 && $namespace !== self::XCARD_NAMESPACE) { 183 184 $propertyName = 'xml'; 185 $value = '<' . $tagName . ' xmlns="' . $namespace . '"'; 186 187 foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) { 188 $value .= ' ' . $attributeName . '="' . str_replace('"', '\"', $attributeValue) . '"'; 189 } 190 191 $value .= '>' . $xmlProperty['value'] . '</' . $tagName . '>'; 192 193 $propertyValue = [$value]; 194 195 $this->createProperty( 196 $parentComponent, 197 $propertyName, 198 $propertyParameters, 199 $propertyType, 200 $propertyValue 201 ); 202 203 continue; 204 } 205 206 // xCard group. 207 if ($propertyName === 'group') { 208 209 if (!isset($xmlProperty['attributes']['name'])) { 210 continue; 211 } 212 213 $this->pointer = &$xmlProperty['value']; 214 $this->parseProperties( 215 $parentComponent, 216 strtoupper($xmlProperty['attributes']['name']) . '.' 217 ); 218 219 continue; 220 221 } 222 223 // Collect parameters. 224 foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) { 225 226 if (!is_array($xmlPropertyChild) 227 || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) 228 continue; 229 230 $xmlParameters = $xmlPropertyChild['value']; 231 232 foreach ($xmlParameters as $xmlParameter) { 233 234 $propertyParameterValues = []; 235 236 foreach ($xmlParameter['value'] as $xmlParameterValues) { 237 $propertyParameterValues[] = $xmlParameterValues['value']; 238 } 239 240 $propertyParameters[static::getTagName($xmlParameter['name'])] 241 = implode(',', $propertyParameterValues); 242 243 } 244 245 array_splice($xmlProperty['value'], $i, 1); 246 247 } 248 249 $propertyNameExtended = ($this->root instanceof VCalendar 250 ? 'xcal' 251 : 'xcard') . ':' . $propertyName; 252 253 switch ($propertyNameExtended) { 254 255 case 'xcal:geo': 256 $propertyType = 'float'; 257 $propertyValue['latitude'] = 0; 258 $propertyValue['longitude'] = 0; 259 260 foreach ($xmlProperty['value'] as $xmlRequestChild) { 261 $propertyValue[static::getTagName($xmlRequestChild['name'])] 262 = $xmlRequestChild['value']; 263 } 264 break; 265 266 case 'xcal:request-status': 267 $propertyType = 'text'; 268 269 foreach ($xmlProperty['value'] as $xmlRequestChild) { 270 $propertyValue[static::getTagName($xmlRequestChild['name'])] 271 = $xmlRequestChild['value']; 272 } 273 break; 274 275 case 'xcal:freebusy': 276 $propertyType = 'freebusy'; 277 // We don't break because we only want to set 278 // another property type. 279 280 case 'xcal:categories': 281 case 'xcal:resources': 282 case 'xcal:exdate': 283 foreach ($xmlProperty['value'] as $specialChild) { 284 $propertyValue[static::getTagName($specialChild['name'])] 285 = $specialChild['value']; 286 } 287 break; 288 289 case 'xcal:rdate': 290 $propertyType = 'date-time'; 291 292 foreach ($xmlProperty['value'] as $specialChild) { 293 294 $tagName = static::getTagName($specialChild['name']); 295 296 if ('period' === $tagName) { 297 298 $propertyParameters['value'] = 'PERIOD'; 299 $propertyValue[] = implode('/', $specialChild['value']); 300 301 } 302 else { 303 $propertyValue[] = $specialChild['value']; 304 } 305 } 306 break; 307 308 default: 309 $propertyType = static::getTagName($xmlProperty['value'][0]['name']); 310 311 foreach ($xmlProperty['value'] as $value) { 312 $propertyValue[] = $value['value']; 313 } 314 315 if ('date' === $propertyType) { 316 $propertyParameters['value'] = 'DATE'; 317 } 318 break; 319 } 320 321 $this->createProperty( 322 $parentComponent, 323 $propertyNamePrefix . $propertyName, 324 $propertyParameters, 325 $propertyType, 326 $propertyValue 327 ); 328 329 } 330 331 } 332 333 /** 334 * Parse a component. 335 * 336 * @param Component $parentComponent 337 * 338 * @return void 339 */ 340 protected function parseComponent(Component $parentComponent) { 341 342 $components = $this->pointer['value'] ?: []; 343 344 foreach ($components as $component) { 345 346 $componentName = static::getTagName($component['name']); 347 $currentComponent = $this->root->createComponent( 348 $componentName, 349 null, 350 false 351 ); 352 353 $this->pointer = &$component; 354 $this->parseVCalendarComponents($currentComponent); 355 356 $parentComponent->add($currentComponent); 357 358 } 359 360 } 361 362 /** 363 * Create a property. 364 * 365 * @param Component $parentComponent 366 * @param string $name 367 * @param array $parameters 368 * @param string $type 369 * @param mixed $value 370 * 371 * @return void 372 */ 373 protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value) { 374 375 $property = $this->root->createProperty( 376 $name, 377 null, 378 $parameters, 379 $type 380 ); 381 $parentComponent->add($property); 382 $property->setXmlValue($value); 383 384 } 385 386 /** 387 * Sets the input data. 388 * 389 * @param resource|string $input 390 * 391 * @return void 392 */ 393 function setInput($input) { 394 395 if (is_resource($input)) { 396 $input = stream_get_contents($input); 397 } 398 399 if (is_string($input)) { 400 401 $reader = new SabreXml\Reader(); 402 $reader->elementMap['{' . self::XCAL_NAMESPACE . '}period'] 403 = 'Sabre\VObject\Parser\XML\Element\KeyValue'; 404 $reader->elementMap['{' . self::XCAL_NAMESPACE . '}recur'] 405 = 'Sabre\VObject\Parser\XML\Element\KeyValue'; 406 $reader->xml($input); 407 $input = $reader->parse(); 408 409 } 410 411 $this->input = $input; 412 413 } 414 415 /** 416 * Get tag name from a Clark notation. 417 * 418 * @param string $clarkedTagName 419 * 420 * @return string 421 */ 422 protected static function getTagName($clarkedTagName) { 423 424 list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName); 425 return $tagName; 426 427 } 428} 429