Mercurial > hg > rc1
comparison vendor/sabre/vobject/lib/Component.php @ 7:430dbd5346f7
vendor sabre as distributed
| author | Charlie Root |
|---|---|
| date | Sat, 13 Jan 2018 09:06:10 -0500 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 6:cec75ba50afc | 7:430dbd5346f7 |
|---|---|
| 1 <?php | |
| 2 | |
| 3 namespace 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 */ | |
| 15 class 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 } |
