Recur.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. <?php
  2. namespace Sabre\VObject\Property\ICalendar;
  3. use Sabre\VObject\InvalidDataException;
  4. use Sabre\VObject\Property;
  5. use Sabre\Xml;
  6. /**
  7. * Recur property.
  8. *
  9. * This object represents RECUR properties.
  10. * These values are just used for RRULE and the now deprecated EXRULE.
  11. *
  12. * The RRULE property may look something like this:
  13. *
  14. * RRULE:FREQ=MONTHLY;BYDAY=1,2,3;BYHOUR=5.
  15. *
  16. * This property exposes this as a key=>value array that is accessible using
  17. * getParts, and may be set using setParts.
  18. *
  19. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  20. * @author Evert Pot (http://evertpot.com/)
  21. * @license http://sabre.io/license/ Modified BSD License
  22. */
  23. class Recur extends Property
  24. {
  25. /**
  26. * Updates the current value.
  27. *
  28. * This may be either a single, or multiple strings in an array.
  29. *
  30. * @param string|array $value
  31. */
  32. public function setValue($value)
  33. {
  34. // If we're getting the data from json, we'll be receiving an object
  35. if ($value instanceof \StdClass) {
  36. $value = (array) $value;
  37. }
  38. if (is_array($value)) {
  39. $newVal = [];
  40. foreach ($value as $k => $v) {
  41. if (is_string($v)) {
  42. $v = strtoupper($v);
  43. // The value had multiple sub-values
  44. if (false !== strpos($v, ',')) {
  45. $v = explode(',', $v);
  46. }
  47. if (0 === strcmp($k, 'until')) {
  48. $v = strtr($v, [':' => '', '-' => '']);
  49. }
  50. } elseif (is_array($v)) {
  51. $v = array_map('strtoupper', $v);
  52. }
  53. $newVal[strtoupper($k)] = $v;
  54. }
  55. $this->value = $newVal;
  56. } elseif (is_string($value)) {
  57. $this->value = self::stringToArray($value);
  58. } else {
  59. throw new \InvalidArgumentException('You must either pass a string, or a key=>value array');
  60. }
  61. }
  62. /**
  63. * Returns the current value.
  64. *
  65. * This method will always return a singular value. If this was a
  66. * multi-value object, some decision will be made first on how to represent
  67. * it as a string.
  68. *
  69. * To get the correct multi-value version, use getParts.
  70. *
  71. * @return string
  72. */
  73. public function getValue()
  74. {
  75. $out = [];
  76. foreach ($this->value as $key => $value) {
  77. $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value);
  78. }
  79. return strtoupper(implode(';', $out));
  80. }
  81. /**
  82. * Sets a multi-valued property.
  83. */
  84. public function setParts(array $parts)
  85. {
  86. $this->setValue($parts);
  87. }
  88. /**
  89. * Returns a multi-valued property.
  90. *
  91. * This method always returns an array, if there was only a single value,
  92. * it will still be wrapped in an array.
  93. *
  94. * @return array
  95. */
  96. public function getParts()
  97. {
  98. return $this->value;
  99. }
  100. /**
  101. * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
  102. *
  103. * This has been 'unfolded', so only 1 line will be passed. Unescaping is
  104. * not yet done, but parameters are not included.
  105. *
  106. * @param string $val
  107. */
  108. public function setRawMimeDirValue($val)
  109. {
  110. $this->setValue($val);
  111. }
  112. /**
  113. * Returns a raw mime-dir representation of the value.
  114. *
  115. * @return string
  116. */
  117. public function getRawMimeDirValue()
  118. {
  119. return $this->getValue();
  120. }
  121. /**
  122. * Returns the type of value.
  123. *
  124. * This corresponds to the VALUE= parameter. Every property also has a
  125. * 'default' valueType.
  126. *
  127. * @return string
  128. */
  129. public function getValueType()
  130. {
  131. return 'RECUR';
  132. }
  133. /**
  134. * Returns the value, in the format it should be encoded for json.
  135. *
  136. * This method must always return an array.
  137. *
  138. * @return array
  139. */
  140. public function getJsonValue()
  141. {
  142. $values = [];
  143. foreach ($this->getParts() as $k => $v) {
  144. if (0 === strcmp($k, 'UNTIL')) {
  145. $date = new DateTime($this->root, null, $v);
  146. $values[strtolower($k)] = $date->getJsonValue()[0];
  147. } elseif (0 === strcmp($k, 'COUNT')) {
  148. $values[strtolower($k)] = intval($v);
  149. } else {
  150. $values[strtolower($k)] = $v;
  151. }
  152. }
  153. return [$values];
  154. }
  155. /**
  156. * This method serializes only the value of a property. This is used to
  157. * create xCard or xCal documents.
  158. *
  159. * @param Xml\Writer $writer XML writer
  160. */
  161. protected function xmlSerializeValue(Xml\Writer $writer)
  162. {
  163. $valueType = strtolower($this->getValueType());
  164. foreach ($this->getJsonValue() as $value) {
  165. $writer->writeElement($valueType, $value);
  166. }
  167. }
  168. /**
  169. * Parses an RRULE value string, and turns it into a struct-ish array.
  170. *
  171. * @param string $value
  172. *
  173. * @return array
  174. */
  175. public static function stringToArray($value)
  176. {
  177. $value = strtoupper($value);
  178. $newValue = [];
  179. foreach (explode(';', $value) as $part) {
  180. // Skipping empty parts.
  181. if (empty($part)) {
  182. continue;
  183. }
  184. $parts = explode('=', $part);
  185. if (2 !== count($parts)) {
  186. throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part);
  187. }
  188. list($partName, $partValue) = $parts;
  189. // The value itself had multiple values..
  190. if (false !== strpos($partValue, ',')) {
  191. $partValue = explode(',', $partValue);
  192. }
  193. $newValue[$partName] = $partValue;
  194. }
  195. return $newValue;
  196. }
  197. /**
  198. * Validates the node for correctness.
  199. *
  200. * The following options are supported:
  201. * Node::REPAIR - May attempt to automatically repair the problem.
  202. *
  203. * This method returns an array with detected problems.
  204. * Every element has the following properties:
  205. *
  206. * * level - problem level.
  207. * * message - A human-readable string describing the issue.
  208. * * node - A reference to the problematic node.
  209. *
  210. * The level means:
  211. * 1 - The issue was repaired (only happens if REPAIR was turned on)
  212. * 2 - An inconsequential issue
  213. * 3 - A severe issue.
  214. *
  215. * @param int $options
  216. *
  217. * @return array
  218. */
  219. public function validate($options = 0)
  220. {
  221. $repair = ($options & self::REPAIR);
  222. $warnings = parent::validate($options);
  223. $values = $this->getParts();
  224. foreach ($values as $key => $value) {
  225. if ('' === $value) {
  226. $warnings[] = [
  227. 'level' => $repair ? 1 : 3,
  228. 'message' => 'Invalid value for '.$key.' in '.$this->name,
  229. 'node' => $this,
  230. ];
  231. if ($repair) {
  232. unset($values[$key]);
  233. }
  234. } elseif ('BYMONTH' == $key) {
  235. $byMonth = (array) $value;
  236. foreach ($byMonth as $i => $v) {
  237. if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) {
  238. $warnings[] = [
  239. 'level' => $repair ? 1 : 3,
  240. 'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!',
  241. 'node' => $this,
  242. ];
  243. if ($repair) {
  244. if (is_array($value)) {
  245. unset($values[$key][$i]);
  246. } else {
  247. unset($values[$key]);
  248. }
  249. }
  250. }
  251. }
  252. // if there is no valid entry left, remove the whole value
  253. if (is_array($value) && empty($values[$key])) {
  254. unset($values[$key]);
  255. }
  256. } elseif ('BYWEEKNO' == $key) {
  257. $byWeekNo = (array) $value;
  258. foreach ($byWeekNo as $i => $v) {
  259. if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) {
  260. $warnings[] = [
  261. 'level' => $repair ? 1 : 3,
  262. 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!',
  263. 'node' => $this,
  264. ];
  265. if ($repair) {
  266. if (is_array($value)) {
  267. unset($values[$key][$i]);
  268. } else {
  269. unset($values[$key]);
  270. }
  271. }
  272. }
  273. }
  274. // if there is no valid entry left, remove the whole value
  275. if (is_array($value) && empty($values[$key])) {
  276. unset($values[$key]);
  277. }
  278. } elseif ('BYYEARDAY' == $key) {
  279. $byYearDay = (array) $value;
  280. foreach ($byYearDay as $i => $v) {
  281. if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) {
  282. $warnings[] = [
  283. 'level' => $repair ? 1 : 3,
  284. 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!',
  285. 'node' => $this,
  286. ];
  287. if ($repair) {
  288. if (is_array($value)) {
  289. unset($values[$key][$i]);
  290. } else {
  291. unset($values[$key]);
  292. }
  293. }
  294. }
  295. }
  296. // if there is no valid entry left, remove the whole value
  297. if (is_array($value) && empty($values[$key])) {
  298. unset($values[$key]);
  299. }
  300. }
  301. }
  302. if (!isset($values['FREQ'])) {
  303. $warnings[] = [
  304. 'level' => $repair ? 1 : 3,
  305. 'message' => 'FREQ is required in '.$this->name,
  306. 'node' => $this,
  307. ];
  308. if ($repair) {
  309. $this->parent->remove($this);
  310. }
  311. }
  312. if ($repair) {
  313. $this->setValue($values);
  314. }
  315. return $warnings;
  316. }
  317. }