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