1<?php
2
3namespace Sabre\VObject\Property\VCard;
4
5use DateTime;
6use DateTimeImmutable;
7use DateTimeInterface;
8use Sabre\VObject\DateTimeParser;
9use Sabre\VObject\InvalidDataException;
10use Sabre\VObject\Property;
11use Sabre\Xml;
12
13/**
14 * DateAndOrTime property.
15 *
16 * This object encodes DATE-AND-OR-TIME values.
17 *
18 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
19 * @author Evert Pot (http://evertpot.com/)
20 * @license http://sabre.io/license/ Modified BSD License
21 */
22class DateAndOrTime extends Property {
23
24    /**
25     * Field separator.
26     *
27     * @var null|string
28     */
29    public $delimiter = null;
30
31    /**
32     * Returns the type of value.
33     *
34     * This corresponds to the VALUE= parameter. Every property also has a
35     * 'default' valueType.
36     *
37     * @return string
38     */
39    function getValueType() {
40
41        return 'DATE-AND-OR-TIME';
42
43    }
44
45    /**
46     * Sets a multi-valued property.
47     *
48     * You may also specify DateTimeInterface objects here.
49     *
50     * @param array $parts
51     *
52     * @return void
53     */
54    function setParts(array $parts) {
55
56        if (count($parts) > 1) {
57            throw new \InvalidArgumentException('Only one value allowed');
58        }
59        if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) {
60            $this->setDateTime($parts[0]);
61        } else {
62            parent::setParts($parts);
63        }
64
65    }
66
67    /**
68     * Updates the current value.
69     *
70     * This may be either a single, or multiple strings in an array.
71     *
72     * Instead of strings, you may also use DateTimeInterface here.
73     *
74     * @param string|array|DateTimeInterface $value
75     *
76     * @return void
77     */
78    function setValue($value) {
79
80        if ($value instanceof DateTimeInterface) {
81            $this->setDateTime($value);
82        } else {
83            parent::setValue($value);
84        }
85
86    }
87
88    /**
89     * Sets the property as a DateTime object.
90     *
91     * @param DateTimeInterface $dt
92     *
93     * @return void
94     */
95    function setDateTime(DateTimeInterface $dt) {
96
97        $tz = $dt->getTimeZone();
98        $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']);
99
100        if ($isUtc) {
101            $value = $dt->format('Ymd\\THis\\Z');
102        } else {
103            // Calculating the offset.
104            $value = $dt->format('Ymd\\THisO');
105        }
106
107        $this->value = $value;
108
109    }
110
111    /**
112     * Returns a date-time value.
113     *
114     * Note that if this property contained more than 1 date-time, only the
115     * first will be returned. To get an array with multiple values, call
116     * getDateTimes.
117     *
118     * If no time was specified, we will always use midnight (in the default
119     * timezone) as the time.
120     *
121     * If parts of the date were omitted, such as the year, we will grab the
122     * current values for those. So at the time of writing, if the year was
123     * omitted, we would have filled in 2014.
124     *
125     * @return DateTimeImmutable
126     */
127    function getDateTime() {
128
129        $now = new DateTime();
130
131        $tzFormat = $now->getTimezone()->getOffset($now) === 0 ? '\\Z' : 'O';
132        $nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This' . $tzFormat));
133
134        $dateParts = DateTimeParser::parseVCardDateTime($this->getValue());
135
136        // This sets all the missing parts to the current date/time.
137        // So if the year was missing for a birthday, we're making it 'this
138        // year'.
139        foreach ($dateParts as $k => $v) {
140            if (is_null($v)) {
141                $dateParts[$k] = $nowParts[$k];
142            }
143        }
144        return new DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]");
145
146    }
147
148    /**
149     * Returns the value, in the format it should be encoded for json.
150     *
151     * This method must always return an array.
152     *
153     * @return array
154     */
155    function getJsonValue() {
156
157        $parts = DateTimeParser::parseVCardDateTime($this->getValue());
158
159        $dateStr = '';
160
161        // Year
162        if (!is_null($parts['year'])) {
163
164            $dateStr .= $parts['year'];
165
166            if (!is_null($parts['month'])) {
167                // If a year and a month is set, we need to insert a separator
168                // dash.
169                $dateStr .= '-';
170            }
171
172        } else {
173
174            if (!is_null($parts['month']) || !is_null($parts['date'])) {
175                // Inserting two dashes
176                $dateStr .= '--';
177            }
178
179        }
180
181        // Month
182        if (!is_null($parts['month'])) {
183
184            $dateStr .= $parts['month'];
185
186            if (isset($parts['date'])) {
187                // If month and date are set, we need the separator dash.
188                $dateStr .= '-';
189            }
190
191        } elseif (isset($parts['date'])) {
192            // If the month is empty, and a date is set, we need a 'empty
193            // dash'
194            $dateStr .= '-';
195        }
196
197        // Date
198        if (!is_null($parts['date'])) {
199            $dateStr .= $parts['date'];
200        }
201
202
203        // Early exit if we don't have a time string.
204        if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) {
205            return [$dateStr];
206        }
207
208        $dateStr .= 'T';
209
210        // Hour
211        if (!is_null($parts['hour'])) {
212
213            $dateStr .= $parts['hour'];
214
215            if (!is_null($parts['minute'])) {
216                $dateStr .= ':';
217            }
218
219        } else {
220            // We know either minute or second _must_ be set, so we insert a
221            // dash for an empty value.
222            $dateStr .= '-';
223        }
224
225        // Minute
226        if (!is_null($parts['minute'])) {
227
228            $dateStr .= $parts['minute'];
229
230            if (!is_null($parts['second'])) {
231                $dateStr .= ':';
232            }
233
234        } elseif (isset($parts['second'])) {
235            // Dash for empty minute
236            $dateStr .= '-';
237        }
238
239        // Second
240        if (!is_null($parts['second'])) {
241            $dateStr .= $parts['second'];
242        }
243
244        // Timezone
245        if (!is_null($parts['timezone'])) {
246            $dateStr .= $parts['timezone'];
247        }
248
249        return [$dateStr];
250
251    }
252
253    /**
254     * This method serializes only the value of a property. This is used to
255     * create xCard or xCal documents.
256     *
257     * @param Xml\Writer $writer  XML writer.
258     *
259     * @return void
260     */
261    protected function xmlSerializeValue(Xml\Writer $writer) {
262
263        $valueType = strtolower($this->getValueType());
264        $parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue());
265        $value = '';
266
267        // $d = defined
268        $d = function($part) use ($parts) {
269            return !is_null($parts[$part]);
270        };
271
272        // $r = read
273        $r = function($part) use ($parts) {
274            return $parts[$part];
275        };
276
277        // From the Relax NG Schema.
278        //
279        // # 4.3.1
280        // value-date = element date {
281        //     xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" }
282        //   }
283        if (($d('year') || $d('month') || $d('date'))
284            && (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) {
285
286            if ($d('year') && $d('month') && $d('date')) {
287                $value .= $r('year') . $r('month') . $r('date');
288            } elseif ($d('year') && $d('month') && !$d('date')) {
289                $value .= $r('year') . '-' . $r('month');
290            } elseif (!$d('year') && $d('month')) {
291                $value .= '--' . $r('month') . $r('date');
292            } elseif (!$d('year') && !$d('month') && $d('date')) {
293                $value .= '---' . $r('date');
294            }
295
296        // # 4.3.2
297        // value-time = element time {
298        //     xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)"
299        //                          ~ "(Z|[+\-]\d\d(\d\d)?)?" }
300        //   }
301        } elseif ((!$d('year') && !$d('month') && !$d('date'))
302                  && ($d('hour') || $d('minute') || $d('second'))) {
303
304            if ($d('hour')) {
305                $value .= $r('hour') . $r('minute') . $r('second');
306            } elseif ($d('minute')) {
307                $value .= '-' . $r('minute') . $r('second');
308            } elseif ($d('second')) {
309                $value .= '--' . $r('second');
310            }
311
312            $value .= $r('timezone');
313
314        // # 4.3.3
315        // value-date-time = element date-time {
316        //     xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?"
317        //                          ~ "(Z|[+\-]\d\d(\d\d)?)?" }
318        //   }
319        } elseif ($d('date') && $d('hour')) {
320
321            if ($d('year') && $d('month') && $d('date')) {
322                $value .= $r('year') . $r('month') . $r('date');
323            } elseif (!$d('year') && $d('month') && $d('date')) {
324                $value .= '--' . $r('month') . $r('date');
325            } elseif (!$d('year') && !$d('month') && $d('date')) {
326                $value .= '---' . $r('date');
327            }
328
329            $value .= 'T' . $r('hour') . $r('minute') . $r('second') .
330                      $r('timezone');
331
332        }
333
334        $writer->writeElement($valueType, $value);
335
336    }
337
338    /**
339     * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
340     *
341     * This has been 'unfolded', so only 1 line will be passed. Unescaping is
342     * not yet done, but parameters are not included.
343     *
344     * @param string $val
345     *
346     * @return void
347     */
348    function setRawMimeDirValue($val) {
349
350        $this->setValue($val);
351
352    }
353
354    /**
355     * Returns a raw mime-dir representation of the value.
356     *
357     * @return string
358     */
359    function getRawMimeDirValue() {
360
361        return implode($this->delimiter, $this->getParts());
362
363    }
364
365    /**
366     * Validates the node for correctness.
367     *
368     * The following options are supported:
369     *   Node::REPAIR - May attempt to automatically repair the problem.
370     *
371     * This method returns an array with detected problems.
372     * Every element has the following properties:
373     *
374     *  * level - problem level.
375     *  * message - A human-readable string describing the issue.
376     *  * node - A reference to the problematic node.
377     *
378     * The level means:
379     *   1 - The issue was repaired (only happens if REPAIR was turned on)
380     *   2 - An inconsequential issue
381     *   3 - A severe issue.
382     *
383     * @param int $options
384     *
385     * @return array
386     */
387    function validate($options = 0) {
388
389        $messages = parent::validate($options);
390        $value = $this->getValue();
391
392        try {
393            DateTimeParser::parseVCardDateTime($value);
394        } catch (InvalidDataException $e) {
395            $messages[] = [
396                'level'   => 3,
397                'message' => 'The supplied value (' . $value . ') is not a correct DATE-AND-OR-TIME property',
398                'node'    => $this,
399            ];
400        }
401
402        return $messages;
403
404    }
405}
406