Component.php 17 KB

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