Component.php 20 KB

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