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