DateTime.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <?php
  2. namespace Sabre\VObject\Property\ICalendar;
  3. use DateTimeInterface;
  4. use DateTimeZone;
  5. use Sabre\VObject\DateTimeParser;
  6. use Sabre\VObject\InvalidDataException;
  7. use Sabre\VObject\Property;
  8. use Sabre\VObject\TimeZoneUtil;
  9. /**
  10. * DateTime property.
  11. *
  12. * This object represents DATE-TIME values, as defined here:
  13. *
  14. * http://tools.ietf.org/html/rfc5545#section-3.3.4
  15. *
  16. * This particular object has a bit of hackish magic that it may also in some
  17. * cases represent a DATE value. This is because it's a common usecase to be
  18. * able to change a DATE-TIME into a DATE.
  19. *
  20. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  21. * @author Evert Pot (http://evertpot.com/)
  22. * @license http://sabre.io/license/ Modified BSD License
  23. */
  24. class DateTime extends Property
  25. {
  26. /**
  27. * In case this is a multi-value property. This string will be used as a
  28. * delimiter.
  29. *
  30. * @var string|null
  31. */
  32. public $delimiter = ',';
  33. /**
  34. * Sets a multi-valued property.
  35. *
  36. * You may also specify DateTime objects here.
  37. */
  38. public function setParts(array $parts)
  39. {
  40. if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) {
  41. $this->setDateTimes($parts);
  42. } else {
  43. parent::setParts($parts);
  44. }
  45. }
  46. /**
  47. * Updates the current value.
  48. *
  49. * This may be either a single, or multiple strings in an array.
  50. *
  51. * Instead of strings, you may also use DateTime here.
  52. *
  53. * @param string|array|DateTimeInterface $value
  54. */
  55. public function setValue($value)
  56. {
  57. if (is_array($value) && isset($value[0]) && $value[0] instanceof DateTimeInterface) {
  58. $this->setDateTimes($value);
  59. } elseif ($value instanceof DateTimeInterface) {
  60. $this->setDateTimes([$value]);
  61. } else {
  62. parent::setValue($value);
  63. }
  64. }
  65. /**
  66. * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
  67. *
  68. * This has been 'unfolded', so only 1 line will be passed. Unescaping is
  69. * not yet done, but parameters are not included.
  70. *
  71. * @param string $val
  72. */
  73. public function setRawMimeDirValue($val)
  74. {
  75. $this->setValue(explode($this->delimiter, $val));
  76. }
  77. /**
  78. * Returns a raw mime-dir representation of the value.
  79. *
  80. * @return string
  81. */
  82. public function getRawMimeDirValue()
  83. {
  84. return implode($this->delimiter, $this->getParts());
  85. }
  86. /**
  87. * Returns true if this is a DATE-TIME value, false if it's a DATE.
  88. *
  89. * @return bool
  90. */
  91. public function hasTime()
  92. {
  93. return 'DATE' !== strtoupper((string) $this['VALUE']);
  94. }
  95. /**
  96. * Returns true if this is a floating DATE or DATE-TIME.
  97. *
  98. * Note that DATE is always floating.
  99. */
  100. public function isFloating()
  101. {
  102. return
  103. !$this->hasTime() ||
  104. (
  105. !isset($this['TZID']) &&
  106. false === strpos($this->getValue(), 'Z')
  107. );
  108. }
  109. /**
  110. * Returns a date-time value.
  111. *
  112. * Note that if this property contained more than 1 date-time, only the
  113. * first will be returned. To get an array with multiple values, call
  114. * getDateTimes.
  115. *
  116. * If no timezone information is known, because it's either an all-day
  117. * property or floating time, we will use the DateTimeZone argument to
  118. * figure out the exact date.
  119. *
  120. * @param DateTimeZone $timeZone
  121. *
  122. * @return \DateTimeImmutable
  123. */
  124. public function getDateTime(?DateTimeZone $timeZone = null)
  125. {
  126. $dt = $this->getDateTimes($timeZone);
  127. if (!$dt) {
  128. return;
  129. }
  130. return $dt[0];
  131. }
  132. /**
  133. * Returns multiple date-time values.
  134. *
  135. * If no timezone information is known, because it's either an all-day
  136. * property or floating time, we will use the DateTimeZone argument to
  137. * figure out the exact date.
  138. *
  139. * @param DateTimeZone $timeZone
  140. *
  141. * @return \DateTimeImmutable[]
  142. * @return \DateTime[]
  143. */
  144. public function getDateTimes(?DateTimeZone $timeZone = null)
  145. {
  146. // Does the property have a TZID?
  147. $tzid = $this['TZID'];
  148. if ($tzid) {
  149. $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root);
  150. }
  151. $dts = [];
  152. foreach ($this->getParts() as $part) {
  153. $dts[] = DateTimeParser::parse($part, $timeZone);
  154. }
  155. return $dts;
  156. }
  157. /**
  158. * Sets the property as a DateTime object.
  159. *
  160. * @param bool isFloating If set to true, timezones will be ignored
  161. */
  162. public function setDateTime(DateTimeInterface $dt, $isFloating = false)
  163. {
  164. $this->setDateTimes([$dt], $isFloating);
  165. }
  166. /**
  167. * Sets the property as multiple date-time objects.
  168. *
  169. * The first value will be used as a reference for the timezones, and all
  170. * the other values will be adjusted for that timezone
  171. *
  172. * @param DateTimeInterface[] $dt
  173. * @param bool isFloating If set to true, timezones will be ignored
  174. */
  175. public function setDateTimes(array $dt, $isFloating = false)
  176. {
  177. $values = [];
  178. if ($this->hasTime()) {
  179. $tz = null;
  180. $isUtc = false;
  181. foreach ($dt as $d) {
  182. if ($isFloating) {
  183. $values[] = $d->format('Ymd\\THis');
  184. continue;
  185. }
  186. if (is_null($tz)) {
  187. $tz = $d->getTimeZone();
  188. $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']);
  189. if (!$isUtc) {
  190. $this->offsetSet('TZID', $tz->getName());
  191. }
  192. } else {
  193. $d = $d->setTimeZone($tz);
  194. }
  195. if ($isUtc) {
  196. $values[] = $d->format('Ymd\\THis\\Z');
  197. } else {
  198. $values[] = $d->format('Ymd\\THis');
  199. }
  200. }
  201. if ($isUtc || $isFloating) {
  202. $this->offsetUnset('TZID');
  203. }
  204. } else {
  205. foreach ($dt as $d) {
  206. $values[] = $d->format('Ymd');
  207. }
  208. $this->offsetUnset('TZID');
  209. }
  210. $this->value = $values;
  211. }
  212. /**
  213. * Returns the type of value.
  214. *
  215. * This corresponds to the VALUE= parameter. Every property also has a
  216. * 'default' valueType.
  217. *
  218. * @return string
  219. */
  220. public function getValueType()
  221. {
  222. return $this->hasTime() ? 'DATE-TIME' : 'DATE';
  223. }
  224. /**
  225. * Returns the value, in the format it should be encoded for JSON.
  226. *
  227. * This method must always return an array.
  228. *
  229. * @return array
  230. */
  231. public function getJsonValue()
  232. {
  233. $dts = $this->getDateTimes();
  234. $hasTime = $this->hasTime();
  235. $isFloating = $this->isFloating();
  236. $tz = $dts[0]->getTimeZone();
  237. $isUtc = $isFloating ? false : in_array($tz->getName(), ['UTC', 'GMT', 'Z']);
  238. return array_map(
  239. function (DateTimeInterface $dt) use ($hasTime, $isUtc) {
  240. if ($hasTime) {
  241. return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : '');
  242. } else {
  243. return $dt->format('Y-m-d');
  244. }
  245. },
  246. $dts
  247. );
  248. }
  249. /**
  250. * Sets the json value, as it would appear in a jCard or jCal object.
  251. *
  252. * The value must always be an array.
  253. */
  254. public function setJsonValue(array $value)
  255. {
  256. // dates and times in jCal have one difference to dates and times in
  257. // iCalendar. In jCal date-parts are separated by dashes, and
  258. // time-parts are separated by colons. It makes sense to just remove
  259. // those.
  260. $this->setValue(
  261. array_map(
  262. function ($item) {
  263. return strtr($item, [':' => '', '-' => '']);
  264. },
  265. $value
  266. )
  267. );
  268. }
  269. /**
  270. * We need to intercept offsetSet, because it may be used to alter the
  271. * VALUE from DATE-TIME to DATE or vice-versa.
  272. *
  273. * @param string $name
  274. * @param mixed $value
  275. *
  276. * @return void
  277. */
  278. #[\ReturnTypeWillChange]
  279. public function offsetSet($name, $value)
  280. {
  281. parent::offsetSet($name, $value);
  282. if ('VALUE' !== strtoupper($name)) {
  283. return;
  284. }
  285. // This will ensure that dates are correctly encoded.
  286. $this->setDateTimes($this->getDateTimes());
  287. }
  288. /**
  289. * Validates the node for correctness.
  290. *
  291. * The following options are supported:
  292. * Node::REPAIR - May attempt to automatically repair the problem.
  293. *
  294. * This method returns an array with detected problems.
  295. * Every element has the following properties:
  296. *
  297. * * level - problem level.
  298. * * message - A human-readable string describing the issue.
  299. * * node - A reference to the problematic node.
  300. *
  301. * The level means:
  302. * 1 - The issue was repaired (only happens if REPAIR was turned on)
  303. * 2 - An inconsequential issue
  304. * 3 - A severe issue.
  305. *
  306. * @param int $options
  307. *
  308. * @return array
  309. */
  310. public function validate($options = 0)
  311. {
  312. $messages = parent::validate($options);
  313. $valueType = $this->getValueType();
  314. $values = $this->getParts();
  315. foreach ($values as $value) {
  316. try {
  317. switch ($valueType) {
  318. case 'DATE':
  319. DateTimeParser::parseDate($value);
  320. break;
  321. case 'DATE-TIME':
  322. DateTimeParser::parseDateTime($value);
  323. break;
  324. }
  325. } catch (InvalidDataException $e) {
  326. $messages[] = [
  327. 'level' => 3,
  328. 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType,
  329. 'node' => $this,
  330. ];
  331. break;
  332. }
  333. }
  334. return $messages;
  335. }
  336. }