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