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