1<?php 2 3namespace Sabre\VObject; 4 5/** 6 * Component 7 * 8 * A component represents a group of properties, such as VCALENDAR, VEVENT, or 9 * VCARD. 10 * 11 * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). 12 * @author Evert Pot (http://evertpot.com/) 13 * @license http://sabre.io/license/ Modified BSD License 14 */ 15class Component extends Node { 16 17 /** 18 * Component name. 19 * 20 * This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD. 21 * 22 * @var string 23 */ 24 public $name; 25 26 /** 27 * A list of properties and/or sub-components. 28 * 29 * @var array 30 */ 31 public $children = array(); 32 33 /** 34 * Creates a new component. 35 * 36 * You can specify the children either in key=>value syntax, in which case 37 * properties will automatically be created, or you can just pass a list of 38 * Component and Property object. 39 * 40 * By default, a set of sensible values will be added to the component. For 41 * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To 42 * ensure that this does not happen, set $defaults to false. 43 * 44 * @param Document $root 45 * @param string $name such as VCALENDAR, VEVENT. 46 * @param array $children 47 * @param bool $defaults 48 * @return void 49 */ 50 function __construct(Document $root, $name, array $children = array(), $defaults = true) { 51 52 $this->name = strtoupper($name); 53 $this->root = $root; 54 55 if ($defaults) { 56 // This is a terribly convoluted way to do this, but this ensures 57 // that the order of properties as they are specified in both 58 // defaults and the childrens list, are inserted in the object in a 59 // natural way. 60 $list = $this->getDefaults(); 61 $nodes = array(); 62 foreach($children as $key=>$value) { 63 if ($value instanceof Node) { 64 if (isset($list[$value->name])) { 65 unset($list[$value->name]); 66 } 67 $nodes[] = $value; 68 } else { 69 $list[$key] = $value; 70 } 71 } 72 foreach($list as $key=>$value) { 73 $this->add($key, $value); 74 } 75 foreach($nodes as $node) { 76 $this->add($node); 77 } 78 } else { 79 foreach($children as $k=>$child) { 80 if ($child instanceof Node) { 81 82 // Component or Property 83 $this->add($child); 84 } else { 85 86 // Property key=>value 87 $this->add($k, $child); 88 } 89 } 90 } 91 92 } 93 94 /** 95 * Adds a new property or component, and returns the new item. 96 * 97 * This method has 3 possible signatures: 98 * 99 * add(Component $comp) // Adds a new component 100 * add(Property $prop) // Adds a new property 101 * add($name, $value, array $parameters = array()) // Adds a new property 102 * add($name, array $children = array()) // Adds a new component 103 * by name. 104 * 105 * @return Node 106 */ 107 function add($a1, $a2 = null, $a3 = null) { 108 109 if ($a1 instanceof Node) { 110 if (!is_null($a2)) { 111 throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); 112 } 113 $a1->parent = $this; 114 $this->children[] = $a1; 115 116 return $a1; 117 118 } elseif(is_string($a1)) { 119 120 $item = $this->root->create($a1, $a2, $a3); 121 $item->parent = $this; 122 $this->children[] = $item; 123 124 return $item; 125 126 } else { 127 128 throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); 129 130 } 131 132 } 133 134 /** 135 * This method removes a component or property from this component. 136 * 137 * You can either specify the item by name (like DTSTART), in which case 138 * all properties/components with that name will be removed, or you can 139 * pass an instance of a property or component, in which case only that 140 * exact item will be removed. 141 * 142 * The removed item will be returned. In case there were more than 1 items 143 * removed, only the last one will be returned. 144 * 145 * @param mixed $item 146 * @return void 147 */ 148 function remove($item) { 149 150 if (is_string($item)) { 151 $children = $this->select($item); 152 foreach($children as $k=>$child) { 153 unset($this->children[$k]); 154 } 155 return $child; 156 } else { 157 foreach($this->children as $k => $child) { 158 if ($child===$item) { 159 unset($this->children[$k]); 160 return $child; 161 } 162 } 163 164 throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component'); 165 166 } 167 168 } 169 170 /** 171 * Returns an iterable list of children 172 * 173 * @return array 174 */ 175 function children() { 176 177 return $this->children; 178 179 } 180 181 /** 182 * This method only returns a list of sub-components. Properties are 183 * ignored. 184 * 185 * @return array 186 */ 187 function getComponents() { 188 189 $result = array(); 190 foreach($this->children as $child) { 191 if ($child instanceof Component) { 192 $result[] = $child; 193 } 194 } 195 196 return $result; 197 198 } 199 200 /** 201 * Returns an array with elements that match the specified name. 202 * 203 * This function is also aware of MIME-Directory groups (as they appear in 204 * vcards). This means that if a property is grouped as "HOME.EMAIL", it 205 * will also be returned when searching for just "EMAIL". If you want to 206 * search for a property in a specific group, you can select on the entire 207 * string ("HOME.EMAIL"). If you want to search on a specific property that 208 * has not been assigned a group, specify ".EMAIL". 209 * 210 * Keys are retained from the 'children' array, which may be confusing in 211 * certain cases. 212 * 213 * @param string $name 214 * @return array 215 */ 216 function select($name) { 217 218 $group = null; 219 $name = strtoupper($name); 220 if (strpos($name,'.')!==false) { 221 list($group,$name) = explode('.', $name, 2); 222 } 223 224 $result = array(); 225 foreach($this->children as $key=>$child) { 226 227 if ( 228 ( 229 strtoupper($child->name) === $name 230 && (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group)) 231 ) 232 || 233 ( 234 $name === '' && $child instanceof Property && strtoupper($child->group) === $group 235 ) 236 ) { 237 238 $result[$key] = $child; 239 240 } 241 } 242 243 reset($result); 244 return $result; 245 246 } 247 248 /** 249 * Turns the object back into a serialized blob. 250 * 251 * @return string 252 */ 253 function serialize() { 254 255 $str = "BEGIN:" . $this->name . "\r\n"; 256 257 /** 258 * Gives a component a 'score' for sorting purposes. 259 * 260 * This is solely used by the childrenSort method. 261 * 262 * A higher score means the item will be lower in the list. 263 * To avoid score collisions, each "score category" has a reasonable 264 * space to accomodate elements. The $key is added to the $score to 265 * preserve the original relative order of elements. 266 * 267 * @param int $key 268 * @param array $array 269 * @return int 270 */ 271 $sortScore = function($key, $array) { 272 273 if ($array[$key] instanceof Component) { 274 275 // We want to encode VTIMEZONE first, this is a personal 276 // preference. 277 if ($array[$key]->name === 'VTIMEZONE') { 278 $score=300000000; 279 return $score+$key; 280 } else { 281 $score=400000000; 282 return $score+$key; 283 } 284 } else { 285 // Properties get encoded first 286 // VCARD version 4.0 wants the VERSION property to appear first 287 if ($array[$key] instanceof Property) { 288 if ($array[$key]->name === 'VERSION') { 289 $score=100000000; 290 return $score+$key; 291 } else { 292 // All other properties 293 $score=200000000; 294 return $score+$key; 295 } 296 } 297 } 298 299 }; 300 301 $tmp = $this->children; 302 uksort( 303 $this->children, 304 function($a, $b) use ($sortScore, $tmp) { 305 306 $sA = $sortScore($a, $tmp); 307 $sB = $sortScore($b, $tmp); 308 309 return $sA - $sB; 310 311 } 312 ); 313 314 foreach($this->children as $child) $str.=$child->serialize(); 315 $str.= "END:" . $this->name . "\r\n"; 316 317 return $str; 318 319 } 320 321 /** 322 * This method returns an array, with the representation as it should be 323 * encoded in json. This is used to create jCard or jCal documents. 324 * 325 * @return array 326 */ 327 function jsonSerialize() { 328 329 $components = array(); 330 $properties = array(); 331 332 foreach($this->children as $child) { 333 if ($child instanceof Component) { 334 $components[] = $child->jsonSerialize(); 335 } else { 336 $properties[] = $child->jsonSerialize(); 337 } 338 } 339 340 return array( 341 strtolower($this->name), 342 $properties, 343 $components 344 ); 345 346 } 347 348 /** 349 * This method should return a list of default property values. 350 * 351 * @return array 352 */ 353 protected function getDefaults() { 354 355 return array(); 356 357 } 358 359 /* Magic property accessors {{{ */ 360 361 /** 362 * Using 'get' you will either get a property or component. 363 * 364 * If there were no child-elements found with the specified name, 365 * null is returned. 366 * 367 * To use this, this may look something like this: 368 * 369 * $event = $calendar->VEVENT; 370 * 371 * @param string $name 372 * @return Property 373 */ 374 function __get($name) { 375 376 $matches = $this->select($name); 377 if (count($matches)===0) { 378 return null; 379 } else { 380 $firstMatch = current($matches); 381 /** @var $firstMatch Property */ 382 $firstMatch->setIterator(new ElementList(array_values($matches))); 383 return $firstMatch; 384 } 385 386 } 387 388 /** 389 * This method checks if a sub-element with the specified name exists. 390 * 391 * @param string $name 392 * @return bool 393 */ 394 function __isset($name) { 395 396 $matches = $this->select($name); 397 return count($matches)>0; 398 399 } 400 401 /** 402 * Using the setter method you can add properties or subcomponents 403 * 404 * You can either pass a Component, Property 405 * object, or a string to automatically create a Property. 406 * 407 * If the item already exists, it will be removed. If you want to add 408 * a new item with the same name, always use the add() method. 409 * 410 * @param string $name 411 * @param mixed $value 412 * @return void 413 */ 414 function __set($name, $value) { 415 416 $matches = $this->select($name); 417 $overWrite = count($matches)?key($matches):null; 418 419 if ($value instanceof Component || $value instanceof Property) { 420 $value->parent = $this; 421 if (!is_null($overWrite)) { 422 $this->children[$overWrite] = $value; 423 } else { 424 $this->children[] = $value; 425 } 426 } else { 427 $property = $this->root->create($name,$value); 428 $property->parent = $this; 429 if (!is_null($overWrite)) { 430 $this->children[$overWrite] = $property; 431 } else { 432 $this->children[] = $property; 433 } 434 } 435 } 436 437 /** 438 * Removes all properties and components within this component with the 439 * specified name. 440 * 441 * @param string $name 442 * @return void 443 */ 444 function __unset($name) { 445 446 $matches = $this->select($name); 447 foreach($matches as $k=>$child) { 448 449 unset($this->children[$k]); 450 $child->parent = null; 451 452 } 453 454 } 455 456 /* }}} */ 457 458 /** 459 * This method is automatically called when the object is cloned. 460 * Specifically, this will ensure all child elements are also cloned. 461 * 462 * @return void 463 */ 464 function __clone() { 465 466 foreach($this->children as $key=>$child) { 467 $this->children[$key] = clone $child; 468 $this->children[$key]->parent = $this; 469 } 470 471 } 472 473 /** 474 * A simple list of validation rules. 475 * 476 * This is simply a list of properties, and how many times they either 477 * must or must not appear. 478 * 479 * Possible values per property: 480 * * 0 - Must not appear. 481 * * 1 - Must appear exactly once. 482 * * + - Must appear at least once. 483 * * * - Can appear any number of times. 484 * * ? - May appear, but not more than once. 485 * 486 * It is also possible to specify defaults and severity levels for 487 * violating the rule. 488 * 489 * See the VEVENT implementation for getValidationRules for a more complex 490 * example. 491 * 492 * @var array 493 */ 494 function getValidationRules() { 495 496 return array(); 497 498 } 499 500 /** 501 * Validates the node for correctness. 502 * 503 * The following options are supported: 504 * Node::REPAIR - May attempt to automatically repair the problem. 505 * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. 506 * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. 507 * 508 * This method returns an array with detected problems. 509 * Every element has the following properties: 510 * 511 * * level - problem level. 512 * * message - A human-readable string describing the issue. 513 * * node - A reference to the problematic node. 514 * 515 * The level means: 516 * 1 - The issue was repaired (only happens if REPAIR was turned on). 517 * 2 - A warning. 518 * 3 - An error. 519 * 520 * @param int $options 521 * @return array 522 */ 523 function validate($options = 0) { 524 525 $rules = $this->getValidationRules(); 526 $defaults = $this->getDefaults(); 527 528 $propertyCounters = array(); 529 530 $messages = array(); 531 532 foreach($this->children as $child) { 533 $name = strtoupper($child->name); 534 if (!isset($propertyCounters[$name])) { 535 $propertyCounters[$name] = 1; 536 } else { 537 $propertyCounters[$name]++; 538 } 539 $messages = array_merge($messages, $child->validate($options)); 540 } 541 542 foreach($rules as $propName => $rule) { 543 544 switch($rule) { 545 case '0' : 546 if (isset($propertyCounters[$propName])) { 547 $messages[] = array( 548 'level' => 3, 549 'message' => $propName . ' MUST NOT appear in a ' . $this->name . ' component', 550 'node' => $this, 551 ); 552 } 553 break; 554 case '1' : 555 if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName]!==1) { 556 $repaired = false; 557 if ($options & self::REPAIR && isset($defaults[$propName])) { 558 $this->add($propName, $defaults[$propName]); 559 } 560 $messages[] = array( 561 'level' => $repaired?1:3, 562 'message' => $propName . ' MUST appear exactly once in a ' . $this->name . ' component', 563 'node' => $this, 564 ); 565 } 566 break; 567 case '+' : 568 if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) { 569 $messages[] = array( 570 'level' => 3, 571 'message' => $propName . ' MUST appear at least once in a ' . $this->name . ' component', 572 'node' => $this, 573 ); 574 } 575 break; 576 case '*' : 577 break; 578 case '?' : 579 if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) { 580 $messages[] = array( 581 'level' => 3, 582 'message' => $propName . ' MUST NOT appear more than once in a ' . $this->name . ' component', 583 'node' => $this, 584 ); 585 } 586 break; 587 588 } 589 590 } 591 return $messages; 592 593 } 594 595} 596